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.