Kevin K.
Kevin K.

Reputation: 57

Rufus-scheduler scheduling twice despite using locks

I'm well aware why this is happening (two ruby runtimes) and that this is a common problem for people who have not read the RS FAQ or searched on SO for this before, but I've spent a couple days trying many prescribed solutions yet my rufus-scheduler continues to invoke twice.

This occurs on production only, running Rails 5.0.6, Puma server, on Heroku.

This is my scheduler.rb:

require 'rufus-scheduler'

a_scheduler = Rufus::Scheduler.new(:lockfile => ".rufus-scheduler-a.lock")
b_scheduler = Rufus::Scheduler.new(:lockfile => ".rufus-scheduler-b.lock")

unless defined?(Rails::Console) || File.split($0).last == 'rake' || !Rails.env.production?
  a_scheduler.cron '0 21 * * *', overlap: false, blocking: true do
    MySidekiqWorker.perform_async unless a_scheduler.down? 
  end

  b_scheduler.every '1h', overlap: false, blocking: true do
    MyOtherSidekiqWorker.perform_async unless b_scheduler.down?
  end
end

I've tried lockfiles, configuring my own scheduler_lock, different parameters for .every and .cron. Moreover, it seems even though I have overlap: false and blocking: true, new instances of MyOtherSidekiqWorker will still be invoked while one is still running.

I must be missing something obvious, thanks for your help.

Upvotes: 3

Views: 576

Answers (1)

jmettraux
jmettraux

Reputation: 3551

So, Heroku dynos not sharing the file system

The .rufus-scheduler-a.lock seen on dyno d0 is not the .rufus-scheduler-a.lock seen on dyno d1.

Your Heroku dynos do not share the same filesystem and also they do not share the same Ruby process and thus not the same rufus-scheduler instance. So overlap: false, blocking: true will not have any effect from dyno d0 to dyno d1.

You could implement a custom locking mechanism for rufus-scheduler taking inspiration from https://github.com/jmettraux/rufus-scheduler#advanced-lock-schemes (probably via the database because it's shared by your Ruby processes) but that will not help with overlap: false and blocking: true.

If you still want to have overlap: false and blocking: true, you could look at https://devcenter.heroku.com/articles/scheduled-jobs-custom-clock-processes and have the scheduling happening in a dedicated process/dyno with rufus-scheduler or Clockwork and have no need for a schedule lock.

The rest of my answer is about your code, not about the double scheduling you are experiencing.

scheduler.down?

b_scheduler.every '1h', overlap: false, blocking: true do
  MyOtherSidekiqWorker.perform_async unless b_scheduler.down?
end

Why do you have this unless b_scheduler.down? if the b_scheduler is down the block will not be executed at all.

This is sufficient:

b_scheduler.every '1h', overlap: false, blocking: true do
  MyOtherSidekiqWorker.perform_async
end

a_scheduler vs b_scheduler

You do not need one scheduler for each job. You can simply write:

require 'rufus-scheduler'                                                   

#scheduler = Rufus::Scheduler.new(lockfile: '.rufus-scheduler.lock')     
scheduler = Rufus::Scheduler.new                                  

unless defined?(Rails::Console) || File.split($0).last == 'rake' || !Rails.env.production?                                                                  
  scheduler.cron '0 21 * * *', overlap: false, blocking: true do            
    MySidekiqWorker.perform_async                                           
  end                                                                       
  scheduler.every '1h', overlap: false, blocking: true do                   
    MyOtherSidekiqWorker.perform_async                                      
  end                                                                       
end

Upvotes: 2

Related Questions