Rooster

Rooster is a Rails plugin providing a daemon for running and controlling scheduled tasks from within your application.

It consists of a scheduler daemon running an EventMachine loop that maintains a rufus-scheduler to keep track of your tasks. An EventMachine-based TCP server listens for control commands to start and stop your tasks.

The idea is to be able to deploy your Rails app with all of the needed functionality for running tasks in the background. A few example of such tasks:

  • Generating nightly reports.
  • Expiring database records.
  • Pulling an RSS feed every hour.

You can accomplish these same tasks using a combination of cron and script/runner or rake tasks, but I never liked that approach.

The Daemons gem combined with the daemon_generator plugin works well, but we didn't like managing an army of daemon processes. Rooster leverage one daemon for the main EventMachine loop, and dynamically schedules/unschedules tasks via the rufus-scheduler.

This has been tested on Rails 2.2, with preliminary Rails 3.0 support

Setup

Install the plugin

script/plugin install git://github.com/findchris/rooster.git

Install required gems

gem sources -a http://gems.github.com # if you haven't already...

sudo gem install daemons rufus-scheduler eventmachine

If you want to be able to use english time descriptions in your scheduled tasks, like:

scheduler.every '3h', :first_at => Chronic.parse('midnight')

then install Chronic:

sudo gem install chronic

If you are using Rails 3, add these lines to your Gemfile file

# For Rooster
gem 'daemons', '1.1.0'
gem 'eventmachine', '0.12.10'
gem 'rufus-scheduler'
gem 'chronic', '0.2.3', :require=> 'chronic' # if using Chronic...

Usage

generate a new scheduled task:

script/generate rooster_task MyTaskName

fire up the daemon in console mode to test it out

ruby lib/rooster/rooster_daemon.rb run

When you're done, get your system admin (or switch hats) to add the daemon to the system start-up, and capistrano deploy scripts, etc. Something like:

ruby /path/to/rails_app/lib/rooster/rooster_daemon.rb start
  OR
rake rooster:launch

Control Server

You can send commands to the control server using various rake tasks. For a list of rake tasks, run:

rake -T rooster

As an alternative, you can connect to the control server like so:

telnet 127.0.0.1 8080

Valid commands are:

  • list (lists a summary of tasks including task names and scheduled status)
  • start task_name (starts the task with a class name of "task_name")
  • stop task_name (stops the task with a class name of "task_name")
  • start_all (starts all of the available tasks)
  • stop_all (stops all of the available tasks)
  • restart task_name (stop/start)
  • kill task_name (kills the specified task if it's currently running and unschedules it)
  • stop_tag tag (stops all tasks with the specified tag)
  • quit (Closes the connection to the control server)
  • exit (kills the EventMachine loop)

Customization

You can configure the EventMachine control server like so (defaults to: => "127.0.0.1", :port => "8080"):

Rooster::Runner.server_options = {:host => "1.2.3.4", :port => "5678"}

When the EventMachine or rufus-scheduler encounter an exception, the module Proc Rooster::Runner.error_handler is called. By default, the exception is logged, but this can be customized like so:

Rooster::Runner.error_handler = lambda { |e| HoptoadNotifier.notify(e) }

Rooster has extensive logging, and by default will use the Rails logger if available, falling back on logging to STDOUT. This can be customized:

Rooster::Runner.logger = Logger.new(STDOUT) # or Logger.new(File.join(Rails.root, "log", "rooster.log"))

By default, all tasks are scheduled when the daemon starts. The can be customized like so:

Rooster::Runner.auto_schedule = false

Also, you can schedule only a subset of tasks by specifying an array of tags for the auto_schedule_tags attribute. This is handy for running different tasks on different servers, for example. An omitted or nil value for the auto_schedule_tags attribute will auto-schedule all tasks available if the auto_schedule flag is set. Example:

Rooster::Runner.auto_schedule      = true
Rooster::Runner.auto_schedule_tags = ['app1', 'other_tag']

Finally, there is some (limited) support for command-line parameters to the rooster daemon. This is especially useful for when run rake commands are cumbersome, such as in a monit "start" command. Currently, you may pass in an "auto_schedule_tags" parameter to the daemon, after including a separator of "--", as in:

ruby lib/rooster/rooster_daemon.rb start -- auto_schedule_tags=app1,other_tag

Deployment

A simple capistrano task for restarting the Rooster daemon can be added to your deploy.rb file:

namespace :rooster do
  desc "Reload Rooster Daemon"
  task :reload, :roles => :rooster do
    rails_env = fetch(:rails_env, "production")
    run "cd #{current_path} && sudo rake RAILS_ENV=#{rails_env} rooster:reload"
  end
end

Notes

Because of the way that forked processes work with the Rails environment, you should ensure that all of your database connections are released after usage, as in:

    ActiveRecord::Base.connection_pool.release_connection

Generated Rooster tasks have this included by default with an ensure block.

Author

Chris Johnson

Contributors

Credits

This project was started by Steven Soroka with his scheduler_daemon plugin. I needed the ability to start/stop individual tasks, and so added an EventMachine-based TCP server to listen for control commands. My initial fork of scheduler_daemon became quite different from the original project, and so I decided to just create a new project altogether. My thanks to Steven for kicking this off in the right direction; the result of a few tweets back and forth.

The dependencies of this plugin are all great projects providing great value to the community: EventMachine, rufus-scheduler, Daemons, and Chronic.