Ruby Schedulers: Whenever vs Sidekiq Cron vs Sidekiq Scheduler

hartator
SerpApi
Published in
8 min readMay 3, 2021

We’ve been fully on board the fact that we’re a Web API at SerpApi. And have been using as much as possible HTTP requests to make things work externally but also internally!

HTTP requests even for internal tasks are indeed great. They are easily debuggable as they are part of our usual workflow. Web servers are already there, carefully monitored, and load balanceable. All programming languages have numerous and super solid libraries for them. Toolings (❤️ cURL) are fabulous. And they allow us to not treat external API requests as second class citizens.

However sometimes you don’t have the choice but to use a good old’ cron jobs. eg, Backups, credit renewals, and other time constrained tasks. We will study here 3 Ruby solutions to schedule tasks: Whenever, sidekiq-cron and sidekiq-scheduler. Whenever gem is Ruby wrapper around classic crontabs jobs. Basically giving you a nicer syntax. Sidekiq-cron and sidekiq-scheduler use a more novel approach and seems to abuse Redis in the background to do the work of scheduling things.

To compare these, we will focus mostly on ease of setting up new tasks, ease of monitoring, and ease of access to logs. Ease of monitoring and logging are particularly important as it’s usually where things bit you in the ass when you set things and they fail silently. Looking at you old backup code that didn’t run for months.

Whenever gem

Repository

Config config/schedule.rb is pretty straightforward:

set :output, "log/cron_log.log"
#
every 1.minute do
rake "users:renew_credits"
end

Then:

whenever --update-crontab

And that’s it!

Running whenever command also takes cares of updating and removing existings crons generated by it in addition of adding new cron jobs. And it’s working as expected:

Logging is a not as straightforward. You’ll have to rely on an external errors monitoring tools like Sentry or Airbrake. Or you’ll have to tail the log/crong_log.log file to get an idea what went wrong:

The pros of this solution are we don’t have to maintain sidekiq workers or a Redis database but it’s lacking an UI and a clear view if cron jobs have been running or not. Let’s check the sidekiq based solutions now!

Sidekiq-Cron gem

Repository

Scheduling a cron job is also kind of straightforward:

class Users::RenewCreditsWorker
include Sidekiq::Worker
def perform(*args)
# Mock a simple job to try out different Ruby on Rails scheduling options
# (Obviously to be run monthly in production not every minute)
User.where(email: 'test@serpapi.com').first.renew_montly_credit
end
end
Sidekiq::Cron::Job.create(name: 'Renew user credit worker - every 1min', cron: '1 * * * *', class: 'Users::RenewCreditsWorker')

Although there is 4–5 different ways to configure this and it’s not super clear if this one is a preferred option or not. Another con: It’s not super clear what happen when “Sidekiq::Cron::Job.create” is run each time the Rails app is restarting. And actually, it’s not loading this file and adding the cron job at all even after restarting the Rails server and sidekiq several time:

Which is a little unexpected. Just calling “Users::RenewCreditsWorker” is enough to load the worker and create the cron job.

As you see, it also triggers a warning about “Redis#exists(key)” usage:

`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer =  true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set `Redis.exists_returns_integer = false`, but this option will be removed in 5.0. (/Users/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sidekiq-cron-1.2.0/lib/sidekiq/cron/job.rb:464:in `block in save')

Not sure reassuring on the quality of the gem but it does add it to the cron tab now though:

However puma does now try to re-create the cron job itself as it’s loading the same “Users::RenewCreditsWorker” class:

Which is not okay. Switching to one of the alternative ways of configuring schedules does seem to work, but you now need an initializer that contains:

schedule_file = "config/schedule.yml"if File.exist?(schedule_file) && Sidekiq.server?
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
end

And a schedule.yml file that contains:

users_renew_credits:
cron: "* * * * *"
class: "Users::RenewCreditsWorker"

And it does get scheduled without any other trickery:

It’s cool to be able to see when was the last job scheduled, to disable/enable the cron job, or to force the cron job to run by just a click. You’ll also have one dedicated page per cron job:

And — most importantly — it does work now:

Notice that the scheduling seems be less on the minute than system cron. It might be due to scheduling with sidekiq-cron is run on a set polling intervals. 30s I believe. Which is not a super big deal if it does work reliability as anyway most cron jobs are meant to be run hourly or daily. However more annoying is the non-straightforward way of configuring it — do we really need several ways of doing this when default already seems to be lacking good instructions ? — and the random warnings we’re getting hint the gem hasn’t been updated in a little bit.

Let’s give shot to sidekiq-scheduler.

Sidekiq-Scheduler gem

Repository

Installation and configuration is very straightforward. Their Readme is way better. Given a file defining a worker:

require 'sidekiq-scheduler'class Users::RenewCreditsWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(*args)
# Mock a simple job to try out different Ruby on Rails scheduling options
# (Obviously to be run monthly in production not every minute)
User.where(email: 'test@serpapi.com').first.renew_montly_credit
end
end

And sidekiq.yml defining a schedule:

:schedule:
users_renew_credits:
every: 1m
class: Users::RenewCreditsWorker

It does autocreate the recurring job at sidekiq startup without any other tickering needed:

2021-05-03T04:38:17.899Z pid=2427 tid=ihv INFO: Scheduling users_renew_credits {"every"=>"1m", "class"=>"Users::RenewCreditsWorker", "queue"=>"default"}

Note that we have same warning as sidekiq-cron bout “Redis#exists(key)” usage:

`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer =  true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set `Redis.exists_returns_integer = false`, but this option will be removed in 5.0. (/Users/ioxtrem/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sidekiq-scheduler-3.0.1/lib/sidekiq-scheduler/redis_manager.rb:99:in `block in schedule_exist?')

It seems both gems were using “Sidekiq.redis { |r| r.exists(:schedules) }”-like syntax that got deprecated for “Sidekiq.redis { |r| r.exists?(:schedules) }”: https://github.com/resque/redis-namespace/pull/171

Sidekiq-scheduler does work right away though:

Notice also the scheduling seems to be more regular than Sidekiq-cron. It might be due to the default polling being way more often. 1s for Sidekiq-scheduler instead 30s of Sidekiq-cron.

The added UI by sidekiq-scheduler is nice but is very minimal:

Sidedekiq-cron UI is indeed better. It contains more details and more actions in the overview page but also separated pages for each recurring job with even more details and more actions. Which matters more than a well written Readme.

In conclusion, all 3 of these gems do the job of adding recurrence to your Ruby or Ruby on Rails application. Whenever uses a classic approach of relying on system cron. It does an excellent job wrapping the syntax of system cron making it Ruby-nice, globbing Rails commands in it making sure they work, and making the adding and removal of recurral logic a breeze. Sidekiq-cron and Sidekiq-scheduler both relies on Sidekiq. It’s a heavy dependency as you’ll need to run Sidekiq workers, Redis and add Sidekiq logic to your application. Sidekiq-cron Readme does a poor job getting you setup — default config just doesn’t work — and default polling 30s is also pretty not aggressive compared to Sidekiq-Scheduler work in both areas. However Sidekiq-cron does have the best UI. It has more details and is actually fun to play with. And as we’ve defined earlier, toolings around monitoring, logging, and quick actions are usually what should matter for long term recurring logic central to your business.

[Edit 05/03/2021] Following this article, Mike, creator of Sidekiq, generously offered us a temporary Enterprise license for Sidekiq to try out their official solution to this. Thank you!

Sidekiq Enterprise Periodic Jobs (Official Sidekiq solution)

Documentation page

After Sidekiq Enterprise installation, config is super simple. Directly inside initializers/sidekiq.rb file:

Sidekiq.configure_server do |config|
config.periodic do |periodic|
periodic.register("* * * * *", "Users::RenewCreditsWorker")
end
end

And it immediately works:

Notice the multiple repetition of “17:59:27” and “18:13:28” jobs. It was probably from my MacBook going to sleep or going idle. I would have a preference for Sidekiq Enterprise Periodic Jobs to ignore and just skip jobs that haven’t be run on time for whatever reason. The previous 3 solutions behaved like this when my MacBook went to sleep and it seemed more reasonable. Indeed you wouldn’t want to run backups 10x at the same time to catch up or reset customer credit 10x. In these scenarii, It’s not useful and might mess things up even further. Notice though this solution seems to be most precise when it does run — Cron jobs like this anyway shouldn’t be run on laptop that can go to sleep to be fair! — with runs happening exactly on the second.

UI is also nice and look like Sidekiq-cron with an overview page:

And a separated page for each periodic job:

--

--

hartator
SerpApi

Passion for beautiful code, lunatic enterprises and ludicrous dreams