Search code examples
herokurufus-scheduler

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.


Solution

  • 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