scheduled email release - ruby-on-rails

what will be the best method of releasing a scheduled email say 2 days before an event in RoR. the event date is going to be stored into the database, and i just want a reminder sent out 2 days prior to event.

you can try this
in your schedule.rb file
every 1.day do
trigger mailer
end
and in mailer method
def mail_setup
if Date.today == event_date - 2.days
mail setup
else
do nothing
end
end

You might have figured this alreay out by yourself. Well...anyway: In cases as the described I always use an external crontab job. In a certain interval (in your case e.g. each day) you dial into your application via
rails runner script.rb
or
rails runner -e class-method
The script or the method then scans the db for pending trigger dates and does whatever it should do, e.g. sending emails.
ps: Of course you have to take care of the correct environment for your rails calls.

Related

Execute a function when a time is equal to a certain value in Rails 6

I am building an online e-commerce store, and I am trying to use rails with action cable to update a product from being out of stock to in-stock at a certain date time e.g 12:00:00 2020-02-19.
The idea is as soon as the time is reached, I want to push a Websocket that the product is now available.
I have tried a few solutions such as:
Thread.new do
while true do
if **SOMETIME** == Time.now
ActionCable.server.broadcast "product_channel",content: "product-in-stock"
end
end
end
The main issue with this approach is that it creates another thread and makes rails unresponsive. Furthermore, if this value is set for say 1 week from now I do not want every user who queries the endpoint to create a brand-new thread running like this.
You have two option use sidekiq jobs or use whenever job gem
https://github.com/mperham/sidekiq/wiki/Scheduled-Jobs
Whenever allow you to set specific day and time, check the documentation for more info
https://github.com/javan/whenever

Rails 5.1, recurring events that user can create

I'm looking for a solution so my users would be able to create recurring events. For example lets say that a user wants to add a post every month on a specific date. Every month on that date a new post would be automatically created with some settings the user will give (title, content etc...)
My app will be pushed on heroku after, I know that heroku handle cron jobs by himslef, is there any option or gem that exist to do that ?
I checked the whenever gem but it's not working with heroku.
EDIT : I bring more infos about what I'm looking for exactly. A user create a post, below this new form I'd like to add a recurring option with a recurrence to select. Every month, week, day. Once the post is created, if the post is recurrent, then the same post is created again according to the selection the user made. The post will be created again and again until the user update the post and stop the recurrence or delete the post.
You could give resque-delayed a try
https://github.com/elucid/resque-delayed/blob/master/README.md
In the current project I use rufus-scheduler
It can be configured to run a task like:
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
scheduler.cron '5 0 * * *' do
# do something every day, five minutes after midnight
# (see "man 5 crontab" in your terminal)
end
You can use https://crontab.guru/ to check the cron scheduler expression.
OR
You can take a look on heroku scheduled jobs

If I schedule a worker to run every day on Heroku, how can I be sure it doesn't run twice or get skipped?

I'm a bit confused as to how Clockwork, Sidekiq, and Redis all fit together on Heroku given that Heroku restarts dynos at least once a day.
Say I have a worker that runs once a day, so it's configured in config/clock.rb as:
module Clockwork
every(1.day, 'Resend confirmation') { ResendConfirmationWorker.perform_async }
end
In this worker, I get all the users who have created an account but haven't confirmed it within two days, and resend a confirmation email to each of them.
class ResendConfirmationWorker
include Sidekiq::Worker
sidekiq_options queue: :resend_confirmation, retry: false
def perform
d = Time.zone.now - 2.days
users = User.where.not(confirmation_sent_at: nil)
.where(confirmed_at: nil)
.where(created_at: d.beginning_of_day..d.end_of_day)
users.find_each do |user|
user.send_confirmation_instructions
end
end
end
Let's say someone signs up on Monday, this job runs on Wednesday, finds them, and sends them a second confirmation email. Then the dyno gets restarted for whatever reason, and the job runs again. They'll get yet another email. Or alternatively, if the restart happens the moment before the job needs to run, then they won't get anything.
How does Clockwork have any concept of jobs longer than 24 hours, given that its “lifespan” in a Heroku dyno? Is there a way to simply manage this limitation without having to constantly save this sort of thing to the database?
If you know that you will execute it every wednesday, I suggest to use Heroku Scheduler (https://devcenter.heroku.com/articles/scheduler). It lets you run specific commands at set time intervals. Less complexity.
IMO you need more information in the database if you want to avoid such issues. A state machine might help or an explicit second_confirmation_send_at column.
That would allow you to write the query in your job like this:
users = User.where('confirmation_sent_at < ?', 2.days.ago)
.where(second_confirmation_send_at: nil)
Then the query doesn't care anymore if it runs multiple times a day, or by accident a day later.

Sending Email with Delayed_Job

I have what I think is a working setup to send emails via Delayed_Job. However, I haven't received my test email and it isn't practical to wait for more to happen with a delay of days. I need to figure out:
What's wrong that's causing the email not to send.
How to test it without waiting days at a time.
I'm new to Delayed_Job, so pardon the newbie mistakes.
Here's the model that includes the send_reminder_emails method. They were fully functional without the .delay(run_at: self.mail_date) bit, so at least I know that much works:
class Reminder < ActiveRecord::Base
belongs_to :user
before_save :create_mail_date
after_save :send_reminder_emails
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
def create_mail_date
#schedule = IceCube::Schedule.new(self.date)
case self.repeating
when "Weekly"
#schedule.add_recurrence_rule(
IceCube::Rule.weekly
)
when "Monthly"
#schedule.add_recurrence_rule(
IceCube::Rule.monthly.day_of_month(self.date.mon)
)
when "Yearly"
#schedule.add_recurrence_rule(
IceCube::Rule.yearly.day_of_year(self.date.yday)
)
end
if self.repeating
self.date = #schedule.next_occurrence(Time.now)
end
self.mail_date = self.date - 7.days
end
private
def send_reminder_emails
if self.reminder
ReminderMailer.delay(run_at: self.mail_date).reminder_send(self.user, self).deliver_now
self.create_mail_date
end
end
handle_asynchronously :send_reminder_emails
end
The references to schedule are via the Ice_Cube gem and all of the date stuff has been tested via my console and is working. Here is my reminder_mailer.rb:
class ReminderMailer < ApplicationMailer
default from: "man#manlyartofbbq.com"
def reminder_send(user, reminder)
#user = user
#reminder = reminder
mail(to: user.email, subject: "Reminder! #{reminder.name} is fast approaching!")
end
end
I installed Delayed_Job step by step from their readme for Rails 4. Any help getting the delayed part of this mailer ironed out is appreciated!
You can use Active Job for sending mails by scheduling the job at a particular time.
Active Job is a framework for declaring jobs and making them run on a variety of queuing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really.
For enqueuing and executing jobs in production you need to set up a queuing backend, that is to say you need to decide for a 3rd-party queuing library that Rails should use. Rails itself only provides an in-process queuing system, which only keeps the jobs in RAM. If the process crashes or the machine is reset, then all outstanding jobs are lost with the default async back-end. This may be fine for smaller apps or non-critical jobs, but most production apps will need to pick a persistent backend.
Active Job has built-in adapters for multiple queuing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for ActiveJob::QueueAdapters.
Once after configuring the queuing adapters, then create Job for sending mails
class RemainderMailerJob < ApplicationJob
queue_as :default
def perform(user, remainder)
ReminderMailer.reminder_send(user, reminder).deliver
end
end
For sending the mail on a specific time you need to enqueue the job and make it perform at a specific time. If you need to send mail 7 days from now then call this piece of code where ever you need.
RemainderMailerJob.set(wait: 1.week).perform_later(user, remainder)
Now this Job will execute after 1 week, which in turn calls the mailer at that time, so your mails will be sent on the specific day.
IF you knew the specific Date Time then you can use
RemainderMailerJob.set(wait_untill: <Specific date>).perform_later(user, remainder)
If you have any doubts regarding the active job kindly refer Active Job Basics
When you send mails using delayed job, there is no need to add 'deliver' or 'deliver_now' when initializing the delayed_job for mailer. So
ReminderMailer.delay(run_at: self.mail_date).reminder_send(self.user, self)
will itself work.
Convert your 'self.mail_date' to also include the time at which you want to send the mail. Passing only date will not work. Basically the run_at should specify both date and time.
Please check the 'delayed_job' github link under the heading "Rails 3 Mailers"
https://github.com/collectiveidea/delayed_job
I have seen this happen when attribute protection is misconfigured. (Usually on legacy apps that left some kludge in when upgrading rails version.)
See if you're able to set the field from the console: Delayed::Job.new(run_at: 1.year.from_now). If not, that's the culprit and you'll need to track down the cause in your app.
If it works, then we still need to verify whether this is an app problem or a DJ bug. Make a new minimal rails app with just a delayed_job table and try to reproduce this. If you can't then there's something else odd in your app.

How to correctly implement recurring tasks with time/frequency set by user

Users subscribe to emails containing the last videos, but they also set when to get those emails.
Subscription(user_id, frequency, day, time, time_zone)
user_id | frequency | day | time | time_zone
1 | daily | null | 16:00 | GMT
2 | weekly | friday | 11:00 | UTC
3 | weekly | monday | 18:00 | EST
How can we send the emails at the exact time and frequency chosen by users in their time zone without screwing up (like sending double emails or missing time)
The only frequencies are daily and weekly, if daily then the day is null.
I use redis as a database for this, let me know how to do this the right way!
I'm going to expand on the answer of fmendez using the resque-scheduler gem.
First, let's create the worker that sends the emails
class SubscriptionWorker
def self.perform(subscription_id)
subscription = Subscription.find subscription_id
# ....
# handle sending emails here
# ....
# Make sure that you don't have duplicate workers
Resque.remove_delayed(SubscriptionWorker, subscription_id)
# this actually calls this same worker but sets it up to work on the next
# sending time of the subscription. next_sending_time is a method that
# we implement in the subscription model.
Resque.enqueue_at(subscription.next_sending_time, SubscriptionWorker, subscription_id)
end
end
In your subscription model, add a next_sending_time method to calculate the next time an email should be sent.
# subscription.rb
def next_sending_time
parsed_time = Time.parse("#{time} #{time_zone}") + 1.day
if frequency == 'daily'
parsed_time
else
# this adds a day until the date matches the day in the subscription
while parsed_time.strftime("%A").downcase != day.downcase
parsed_time += 1.day
end
end
end
I have used delayed_job for similar tasks in the past. Probably you can use the same technique with resque. Essentially, you have to schedule the next job at the end of the current job.
class Subscription < ActiveRecord::Base
after_create :send_email
def send_email
# do stuff and then schedule the next run
ensure
send_email
end
handle_asynchronously :send_email, :run_at => Proc.new{|s| s.deliver_at }
def daily? (frequency == "daily");end
def max_attempts 1;end
def time_sec
hour,min=time.split(":").map(&:to_i)
hour.hours + min.minutes
end
def days_sec
day.nil? ? 0 : Time::DAYS_INTO_WEEK[day.to_sym].days
end
def interval_start_time
time = Time.now.in_time_zone(time_zone)
daily? ? time.beginning_of_day : time.beginning_of_week
end
def deliver_at
run_at = interval_start_time + days_sec + time_sec
if time.past?
run_at = daily? ? run_at.tomorrow : 1.week.from_now(run_at)
end
run_at
end
end
Rescheduling caveats
Update the code to handle cycle termination. You can handle this by adding a boolean column called active (set it to true by default). To disable the subscription, set the column to false.
def send_email
return unless active?
# do stuff and then schedule the next run
ensure
send_email if active?
end
Set the max_attempts for the job to 1. Otherwise you will flood the queue. In the solution above, the jobs for send_email will be attempted once.
This is more of a system level problem than being specific to ruby.
First off, store all your time internally as GMT. Timezones are not real, other than being a preference setting (in the user's table) that offsets the time in the view. The coordinate system in which the math is done should be consistent.
Then each frequency corresponds to a cronjob setting: daily, monday, tuesday, etc. Really, from the data in table, you don't need two columns, unless you see this as changing.
Then, when the cron fires, use a scheduler (like linux AT) to handle when the email goes out. This is more of a system level scheduling problem, or at least, I'd trust the system more to handle this. It needs to handle the case where the system is restarted, including the services and app.
One caveat however, is that if you are sending a large volume of mail, you really can't guarantee that the mail will be sent according to preference. You may actually have to throttle it back to avoid getting dumped/blocked (to say, 1000/messages spread over an hour). Different networks will have different thresholds of use.
So basically:
cron -> at -> send mail
I think you can leverage this gem for this purpose: https://github.com/bvandenbos/resque-scheduler
Regards,
You could use the DelayedJob or Resque Gems, which represent each instance of a scheduled task as a row in a database table. There are methods for triggering, scheduling, and withdrawing tasks from the calendar.
I've just implemented this for my project, I've found a really easy way to do it is to use the Whenever Gem (found here https://github.com/javan/whenever).
To get started, go to your app and put
gem 'whenever'
then use:
wheneverize .
in the terminal. This will create a schedule.rb file in your config folder.
You put your rules in your schedule.rb (shown below) and let them call certain methods - for instance, mine calls the model method DataProviderUser.cron which will run whatever code I have there.
Once you've created this file, to start the cron job, on the command line use:
whenever
whenever -w
and
whenever -c
stops/clears the cron jobs.
The gems documentation on github is really useful but I recommend you set the output to your own log file (as I've done below). Hope that helps :)
in my schedule.rb I have:
set :output, 'log/cron.log'
every :hour do
runner "DataProviderUser.cron('hourly')"
end
every :day do
runner "DataProviderUser.cron('daily')"
end
every '0 0 * * 0' do
runner "DataProviderUser.cron('weekly')"
end
every 14.days do
runner "DataProviderUser.cron('fortnightly')"
end
every :month do
runner "DataProviderUser.cron('monthly')"
end

Resources