Using the Whenever gem to manage scheduled cron jobs without installing it on the server

September 30, 2009

I've been using Javan's Whenever gem to manage scheduled jobs in my project and its fantastic!! There are many existing resources where you can learn more (readme, railscast google group) but I'd like to describe the specific way I'm using it


  • When the gem is not installed on my server

  • How administrators can use Capistrano to both schedule and unschedule your jobs

  • How to use a library such as the Oracle client that requires certain environment variables



At the end of the day we want to have 2 capistrano tasks we can run to have cron call a rake task of ours on the schedule we want.

  cap schedule_jobs
  cap unschedule_jobs



If we want to get fancy and pass a custom configuration argument that will be passed into the rake task.

  cap schedule_jobs SOME_CONFIGURATION=false



When the gem is not installed on my server


Let's start with the Capfile

#Capfile
desc "Schedule the jobs"
task :schedule_job, :roles => :app, :only => { :primary => true } do
  some_configuration = ENV['SOME_CONFIGURATION'] || true #default to true
  arguments = ["RAILS_ENV=#{rails_env}",
               "APP_PATH=#{current_path}",
               "SOME_CONFIGURATION=#{some_configuration}"].join(' ')
  run "cd #{current_path} && #{rake} whenever:update_crontab #{arguments}"
end

desc "Unschedule the jobs"
task :unschedule_job, :roles => :app, :only => { :primary => true } do
  run "cd #{current_path} && #{rake} whenever:update_crontab  UNSCHEDULE=true"
end



How does this differ from the example on the whenever site? Since the gem is not installed on the server we cannot call whenever from the command line so invoke the whenever:update_crontab rake task instead and to allow administrators to easily disable the scheduled jobs we define the unschedule_jobs capistrano task. Let's take a look at the whenever:update_crontab Rake task that gets this all done.

#lib/tasks/whenever.rake
namespace :whenever do
  desc "updates crontab with our scheduled jobs"  
  task :update_crontab => :load_whenever_gem do     
    Whenever::CommandLine.execute({:update=>true, :identifier=>'YOUR_APP_NAME'})
  end

  task :load_whenever_gem do     
    begin
      gem_dir_root = "#{RAILS_ROOT}/vendor/gems/"
      chronic_gem_dir = Dir["#{RAILS_ROOT}/vendor/gems/*"].detect do |subdir|
        subdir.gsub(gem_dir_root,"") =~ /^(\w+-)?chronic-(\d+)/ && File.exist?("#{subdir}/lib/chronic.rb")
      end
      require "#{chronic_gem_dir}/lib/chronic"

      whenever_gem_dir = Dir["#{RAILS_ROOT}/vendor/gems/*"].detect do |subdir|
        subdir.gsub(gem_dir_root,"") =~ /^(\w+-)?whenever-(\d+)/ && File.exist?("#{subdir}/lib/whenever.rb")
      end
      require "#{whenever_gem_dir}/lib/whenever"

    rescue MissingSourceFile => e
      raise "Cannot find Whenever or Chronic : #{e}"
    end
  end
end



The actual whenever:update_crontab task just does the same the command line does but unless you add config.gem 'whenever' to your environment (which I don't since whenever is not needed by my app at runtime) we also have the other task that loads whenever and chronic from the vendor/gems directory.

At this point we've gotten Capistrano calling a rake task to invoke whenever even though the gem is localized in my application but not installed on the server.

How administrators can use capistrano to both schedule and unschedule your jobs



The whenever gem does not have any support for unscheduling but it will schedule whatever is included in your schedule.rb file so if that file tells it to schedule nothing that's the same as unscheduling. Its easy to do that by wrapping the entire file with unless ENV['UNSCHEDULE'] (remember the UNSCHEDULE parameter we passed in the Capfile?)

#config/schedule.rb
unless ENV['UNSCHEDULE']

  module MyApp
    module Job
      class CronRakeTask < Whenever::Job::Default
        def output
          path_required
          "cd #{@path} && /usr/bin/env /usr/local/bin/cron_rake #{task} SOME_CONFIGURATION=#{ENV['SOME_CONFIGURATION']} RAILS_ENV=#{@environment}"
        end      
      end
    end
  end

  set :path,        ENV['APP_PATH'] || RAILS_ROOT 
  set :environment, RAILS_ENV || 'production'
  every 1.day, :at => '10:00pm' do
    command 'do_something', :class       => MyApp::Job::CronRakeTask, 
                            :environment => @environment, 
                            :path        => @path
  end
end



What's going on with the weird CronRakeTask? This brings us to the final point

How to use a library such as the Oracle client that requires certain environment variables



Cron loads its environment settings differently than an interactive shell and typically does not have all the environment variables you may have in your profile file. You could solve this by adding them to the top of your crontab file but I prefer to leave that file as simple as possible and create a wrapper script to call instead of rake. Basically the CronRakeTask does the same as Whenever's built in RakeTask except it calls /usr/local/bin/cron_rake instead of rake.

The cron_rake file just sets the environment variables I need then calls rake.

#!/bin/bash

export PATH=/usr/local/lib/ruby-enterprise/bin:$PATH
export ORACLE_HOME=/opt/oracle
export LD_LIBRARY_PATH=/opt/oracle:$LD_LIBRARY_PATH

rake $@



As I said in the beginning since I've been using the Whenever gem I no longer need to manually edit my crontab files ever and I can enable my jobs as part of my normal deployment process. Its wonderful and I think everyone should use it!