While the unicorn setup I described there runs nice and stable, following a similar approach with an application running on Passenger / mod_rails proved to be quite unstable due to Passenger’s more dynamic process handling. Whenever Passenger decides to kill one of your application processes due to low load or a given max requests per process limit, it may happen that decides to kill exactly that process where the scheduler is running. In this case you end up without scheduler until a new application process is spawned because of increased load. So a separate scheduling process was in order. Still very happy with the scheduling capabilities of the rufus-scheduler gem I started to build a daemon around it. Using Dante this turned out to be very easy.
A while later I decided to extract the tiny bits and pieces of code needed to glue Dante and Rufus together and integrate them into a Rails app into it’s own gem for easier re-use: Cronic.
Why build another job scheduler for Rails?
Yes I know there already are lots of job scheduling solutions out there, however none seemed to fit my needs:
- don’t spawn a new Rails runtime every time a job is run. This rules out Unix cron and any solutions based on it, like the well-known whenever
- support for cron patterns - while considered unreadable by some people, I think it is a quite versatile syntax, plus any sysadmin will feel familiar with it
- provide a standardized place for job schedules, just like
/etc/cron.d/on Unix systems
- easy setup for a new application
- Capistrano integration for automatic start/stop/restart at deployment time
- Airbrake integration for error reporting in case a job fails. I dont want to handle this in each and every job. Just throw an error and be sure it will end up in a place where you will take notice.
- fine grained control over which jobs may run in parallel and which may not
- ability to avoid overlapping of subsequent runs of the same job in case a run takes longer than the configured interval for the job
While the last points can easily be implemented inside the jobs in question, i.e. using lock files, just like the Airbrake reporting it makes sense to have that built into the scheduler itself.
And to be honest - using Dante and Rufus Scheduler it was so dead easy to put this together, that most of the work went into boilerplate stuff like gemspec, docs and capistrano recipes.
I’ll just quote the readme here.
Add Cronic to your Gemfile and run Bundler
echo "gem 'cronic'" >> Gemfile bundle install
Run the Rails generator and create job definitions
rails g cronic
This will set up
script/cronic, which you will use to start / stop the
daemon. It also creates the
config/cronic.d directory where you will
store your job definitions. Have a look at
to get an idea of how to define your jobs. For more information, be sure
to visit the Rufus-Scheduler documentation. Every method that is
available on a Rufus::Scheduler instance can be called in the job
definition files located in
script/cronic -d -l log/cronic.log -P tmp/pids/cronic.pid
This will run cronic daemonized, logging to log/cronic.log, with a pid file located in tmp/pids. To run in the forground for testing purposes, just run the script without any parameters.
In order to stop the daemon, run
script/cronic -k -P tmp/pids/cronic.pid
And that’s it. Cronic comes with Capistrano recipes for automatic stop/start upon deployment, head over to the github repo for detailed setup instructions.