Moving from heroku scheduler to clockwork and sidekiq
We recently moved away from heroku scheduler and start using clockwork and with ActiveJob. The added value of such a move is massive:
- Better way to track changes in the scheduler
- Easier to get errors from failing tasks
- We could run any task at any time and with any interval
We wanted to configure clockwork in a way we could:
- Avoid executing a tasks before the previous run completed
- Catch any exception during a task execution
- We didn’t want to loose the ability to run tasks manually on heroku
The way we setup the project is pretty interesting and this is what I’d like to share with you, my amazing reader.
Clockwork
Just add the gem to your project
gem 'clockwork'
bundle and create a scheduler.rb file on the root directory for you project. Our scheduler.rb file looks like this:
# scheduler.rb
require File.expand_path('../config/boot', __FILE__)
require File.expand_path('../config/environment', __FILE__)
require 'clockwork'
module Clockwork
every(10.minutes, 'Carwow: Recalculate stats') do
ScheduledTasks::Carwow::RecalculateStats.perform_unique_later
end
error_handler do |error|
Bugsnag.notify(error)
end
end
This will execute the Carwow::RecalculateStats active job task every 10 minutes. The error handler will notify us in case we’ve got any problem in the scheduled task invocation (e.g. if there’s a typo in the task name)
Active job task
This is how our RecalculateStats job looks like:
# app/jobs/scheduled_tasks/carwow/recalculate_stats.rb
module ScheduledTasks
module Carwow
class RecalculateStats < ScheduledTask
def perform
StatsGenerator.new.recalculate
end
end
end
end
There’s some magic happening in the ScheduledTask class:
# app/jobs/scheduled_tasks/scheduled_task.rb
module ScheduledTasks
class ScheduledTask < ActiveJob::Base
queue_as :scheduled_tasks
rescue_from(Exception) do |e|
Bugsnag.notify(e)
end
def self.perform_unique_later(*args)
if self.task_already_scheduled?
logger.warn "Task #{self.to_s} already enqueued/running."
return
end
self.perform_later(*args)
end
def self.task_already_scheduled?
job_type = self.to_s
queue_name = 'quotes_site_scheduled_tasks'
q = Sidekiq::Queue.new(queue_name)
is_enqueued = q.any? { |j| j['args'][0]['job_class'] == job_type }
workers = Sidekiq::Workers.new
is_running = workers.any? do |x, y, work|
work['queue'] == queue_name &&
work['payload']['args'][0]['job_class'] == job_type
end
is_enqueued || is_running
end
end
end
perform_unique_later checks against our sidekiq queue if the job is already there, this is to prevent long running jobs to be executed multiple times and start overlapping.
As you can see we also catch any exception and we send it to Bugsnag, look into it. This also prevents the task to be executed again and to stop retrying failing tasks.
What’s missing?
To run the scheduled task manually on heroku, and to test them on our local machine we created some rake tasks dinamically based on the tasks defined above:
# lib/tasks/scheduled_tasks.rake
namespace :scheduled_tasks do
require "./app/jobs/scheduled_tasks/scheduled_task"
Dir[File.join('.', "app/jobs/scheduled_tasks/**/*.rb")].each{ |f| require f }
classes = ObjectSpace.each_object(Class).select{|klass| klass < ScheduledTasks::ScheduledTask }
classes.each do |klass|
class_name = klass.to_s
task_name = class_name.gsub("ScheduledTasks::", "").gsub("::", ":").underscore
desc "Runs #{class_name}"
task task_name => :environment do
puts "Executing job #{klass.to_s}"
klass.perform_now
end
end
end
With this bit of code we can now execute the previously defined rake task invoking
$ rake scheduled_tasks:carwow:recalculate_stats
That’s all.
Originally published at underthehood.carwow.co.uk on January 8, 2016.
Interested in making an Impact? Join the carwow-team!
Feeling social? Connect with us on Twitter and LinkedIn :-)