How to create a report scheduler? - ruby-on-rails

A user can choose (and continually edit) which days they want to receive certain reports:
3 report options
7 possible days (Monday - Sunday)
If a user can receive any of the 3 reports on any day of the week, or at the end of the month, what is the best way to store the data in the database?
User 1's report schedule = report 1 monday and tuesday, report 2 monday, report 3 at the end of every month
My first thought was to use a Schedule model and have boolean values but it soon got out of control:
Schedule model
attr_accessible :report_one_monday, :report_one_tuesday, :report_one_wednesday, :report_one_thursday, :report_one_friday, :report_one_saturday, :report_one_sunday
:report_two_monday, :report_two_tuesday, :report_two_wednesday, :report_two_thursday, :report_two_friday, :report_two_saturday, :report_two_sunday
:report_three_monday, :report_three_tuesday, :report_three_wednesday, :report_three_thursday, :report_three_friday, :report_three_saturday, :report_three_sunday
:report_one_end_of_month etc
I want to use heroku scheduler to run a rake task daily to see which reports it must send. The end of the month task would look something like:
task :send_reports => :environment do
if Date.today == Date.today.end_of_month
Business.where(report_one_end_of_month: true).each do |business|
MyMailer.send_month_end_report(business).deliver
end
end
end

Have a look at resque and resque-scheduler which is super set of refus scheduler.
We are using this tool to generate reports in background and email it to user from long time which will definitely solve your problem.
Resque is the redis based tool to run background jobs and resque-scheduler is of course scheduler, which can accept cron schedule to schedule the resque job in future.

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.

How to do a rake task at a specific time of day per user's timezone in rails?

I want to perform some maintenance on each user's information, but I want it to only happen around midnight per the user's timezone. The user's table has a time_zone column that defaults to Eastern.
So far I have the following rake task set to run every hour:
task update_user_stuff: :environment do
users = []
User.all.each do |user|
b = Time.now - 29.minutes
e = Time.now + 30.minutes
if (b..e).cover?(Time.now.in_time_zone(user.time_zone).beginning_of_day)
users << user
end
end
unless users.blank?
users.each do |user|
# user maintenance here
end
end
end
That code seems to work in my testing with a testing database of 20 users, taking about 4 seconds to complete. My concern is that on a production server with thousands of users, it'll be inefficient and that relying on manually setting a time window (lines 4-5) will lead to problems.
What's a better way to write this task?
Edit for even more clarity: I know how to run this task periodically. I'm asking if the task itself can be written better.
You can use the whenever gem https://github.com/javan/whenever which uses cron to run tasks.
You can setup a whenever cron job to go through all users and check their timezones. If the time matches, then you can something specific for that user.
Example: Setup whenever cron job to run task every hour. This task calls a method that checks user's timezones and if the timezone matches, it a condition then it calls another rake task or method for that specific user.

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