Multi timezone task scheduling in Rails (using whenever) - ruby-on-rails

We're currently using 'whenever' to schedule jobs in a Rails project. The system is expanding to support users in multiple timezones (timezone is stored in User model) and we would like to send users an email at a specific time of THEIR day.
Any ideas on the best pattern to achieve this? I'm foreseeing 24 (or more - there are half timezones) 'whenever' tasks per email job each with a different timezone filter on the user table.
Any ideas for a cleaner solution? A better scheduler than whenever perhaps? Something that will create the 24/48 cronjobs and call a callback passing a timezone or a UTC offset? Something like that.

I am not familiar with 'whenever' but I had a similar challenge and used clockwork
My problem was that I had to run a process at midnight for each of my customers. And like you, my customers can be anywhere in the world so it had to be ran at midnight their time.
The process of running the job at midnight looked something like this:
TZInfo::Timezone.all_country_zone_identifiers.each do |zone|
every(1.day, {time_zone: zone}, at: '00:00', tz: "#{zone}") do |job_attr|
time_zone = job_attr[:time_zone]
YourJob.perform_later(time_zone)
end
end
The class of 'YourJob' will just find all my customers with that time_zone and perform a job.
Hope that helps.

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

Rails: Simplest way of sending mail when X is true

So I've been looking for the simplest way to send an e-mail when X column of Payments table in the database is == 'condition'. Basically what I want is to add a payment and set a date like 6 months. When 6 months have passed I want to send the mail. I've seen many solutions like using Whenever cron jobs and others but I want to know the absolute simplest way (perhaps using Rails only without relying on outside source) to keep my application light and clean. I was thinking I could use the auto generated created_at to evaluate when x time has passed.
Since you have a column in your db for the time to send email, make it a datetime datatype and you can set the email date as soon as the event payment event is created. Then, you can have a rake task where,
range = Time.now.beginning_of_day..Time.now.end_of_day
Payment.where(your_datetime_custom_column: range).each do |payment|
payment.user.send_email
end
and you can run this task everyday from the scheduler.
The "easiest" way is to use Active Job in conjunction with a state machine:
EmailJob.set(wait: 6.months).perform_later(user.id) if user.X_changed?
The problem with this is that the queue will accumulate jobs since jobs don't get handled right away. This may lead to other performance issues since there are now more jobs to scan and they're taking up more memory.
Cron jobs are well suited for this kind of thing. Depending on your hosting platform, there may be various other ways to handle this; for example, Heroku has Heroku Scheduler.
There are likely other ways to schedule repeating tasks without cron, such as this SO answer.
edit: I did use a gem once called 'fist_of_fury', but it's not currently maintained and I'm not sure how it would perform in a production environment. Below are some snippets for how I used it in a rails project:
in Gemfile
gem 'fist_of_fury'
in config/initializers/fist_of_fury.rb
# Ensure the jobs run only in a web server.
if defined?(Rails::Server)
FistOfFury.attack! do
ObserveAllJob.recurs { minutely(1) }
end
end
in app/jobs/observe_all_job.rb
class ObserveAllJob
include SuckerPunch::Job
include FistOfFury::Recurrent
def perform
::Task.all.each(&:observe)
end
end

scheduled email release

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.

Rails 3.1/rake - datespecific tasks without queues

I want to give my users the option to send them a daily summary of their account statistics at a specific (user given) time ....
Lets say following model:
class DailySummery << ActiveRecord::Base
# attributes:
# send_at
# => 10:00 (hour)
# last_sent_at
# => Time of the last sent summary
end
Is there now a best practice how to send this account summaries via email to the specific time?
At the moment I have a infinite rake task running which checks permanently if emails are available for sending and i would like to put the dailysummary-generation and sending into this rake task.
I had a thought that I could solve this with following pseudo-code:
while true
User.all.each do |u|
u.generate_and_deliver_dailysummery if u.last_sent_at < Time.now - 24.hours
end
sleep 60
end
But I'm not sure if this has some hidden caveats...
Notice: I don't want to use queues like resq or redis or something like that!
EDIT: Added sleep (have it already in my script)
EDIT: It's a time critical service (notification of trade rates) so it should be as fast as possible. Thats the background why I don't want to use a queue or job based system. And I use Monit to manage this rake task, which works really fine.
There's only really two main ways you can do delayed execution. You run the script when an user on your site hits a page, which is inefficient and not entirely accurate. Or use some sort of background process, whether it's a cron job or resque/delayed job/etc.
While your method of having an rake process run forever will work fine, it's inefficient because you're iterating over users 24/7 as soon as it finishes, something like:
while true
User.where("last_sent_at <= ? OR last_sent_at = ?", 24.hours.ago, nil).each do |u|
u.generate_and_deliver_dailysummery
end
sleep 3600
end
Which would run once an hour and only pull users that needed an email sent is a bit more efficient. The best practice would be to use a cronjob though that runs your rake task though.
Running a task periodically is what cron is for. The whenever gem (https://github.com/javan/whenever) makes it simple to configure cron definitions for your app.
As your app scales, you may find that the rake task takes too long to run and that the queue is useful on top of cron scheduling. You can use cron to control when deliveries are scheduled but have them actually executed by a worker pool.
I see two possibilities to do a task at a specific time.
Background process / Worker / ...
It's what you already have done. I refactored your example, because there was two bad things.
Check conditions directly from your database, it's more efficient than loading potential useless data
Load users by batch. Imagine your database contains millions of users... I'm pretty sure you would be happy, but not Rails... not at all. :)
Beside your code I see another problem. How are you going to manage this background job on your production server? If you don't want to use Resque or something else, you should consider manage it another way. There is Monit and God which are both a process monitor.
while true
# Check the condition from your database
users = User.where(['last_sent_at < ? OR created_at IS NULL', 24.hours.ago])
# Load by batch of 1000
users.find_each(:batch_size => 1000) do |u|
u.generate_and_deliver_dailysummery
end
sleep 60
end
Cron jobs / Scheduled task / ...
The second possibility is to schedule your task recursively, for instance each hour or half-hour. Correct me if I'm wrong, but do your users really need to schedule the delivery at 10:39am? I think that let them choose the hour is enough.
Applying this, I think a job fired each hour is better than an infinite task querying your database every single minute. Moreover it's really easy to do, because you don't need to set up anything.
There is a good gem to manage cron task with the ruby syntax. More infos here : Whenever
You can do that, you'll need to also check for the time you want to send at. So starting with your pseudo code and adding to it:
while true
User.all.each do |u|
if u.last_sent_at < Time.now - 24.hours && Time.now.hour >= u.send_at
u.generate_and_deliver_dailysummery
# the next 2 lines are only needed if "generate_and_deliver_dailysummery" doesn't sent last_sent_at already
u.last_sent_at = Time.now
u.save
end
end
sleep 900
end
I've also added the sleep so you don't needlessly hammer your database. You might also want to look into limiting that loop to just the set of users you need to send to. A query similar what Zachary suggests would be much more efficient than what you have.
If you don't want to use a queue - consider delayed job (sort of a poor mans queue) - it does run as a rake task similar to what you are doing
https://github.com/collectiveidea/delayed_job
http://railscasts.com/episodes/171-delayed-job
it stores all tasks in a jobs table, usually when you add a task it queues it to run as soon as possible, however you can override this to delay it until a specific time
you could convert your DailySummary class to DailySummaryJob and once complete it could re-queue a new instance of itself for the next days run
How did you update the last_sent_at attribute?
if you use
last_sent_at += 24.hours
and initialized with last_sent_at = Time.now.at_beginning_of_day + send_at
it will be all ok .
don't use last_sent_at = Time.now . it is because there may be some delay when the job is actually done , this will make the last_sent_at attribute more and more "delayed".

Resources