cloud_foundry labs rails

Scheduling tasks on Cloud Foundry

In migrating our internal apps from Heroku over to Cloud Foundry, we’ve had to work around the fact that Cloud Foundry doesn’t (yet!) have a feature equivalent to Heroku’s scheduler. Fortunately, it’s pretty easy to set up a scheduler – one that’s even got a finer level of scheduling granularity. Below, I go through the details of creating a scheduler for Rake tasks. It’s abstract enough that it should work for other types of jobs… basically, anything that you’d type directly into the command line.

At a high level, our goal here is to create 2 additional apps. One of these is responsible for scheduling tasks, and the other is responsible for queueing and executing these tasks. I chose to use Clockwork for scheduling the jobs, and Resque for queueing and processing the jobs. There are other options for each of these – popular alternatives to Clockwork include Whenever, Rufus-scheduler and resque-scheduler, and you could also use DelayedJob or Sidekiq in lieu of Resque.

Anyway, step numero 0 is to add these 2 gems to your Gemfile.

gem 'resque'
gem 'clockwork'

In app/models/scheduled_job_worker.rb, use the code below to create a worker capable of executing system calls. One benefit of this implementation is that it will work with any existing Rake task you have; there’s no need to create a new class for each job you’d like to schedule.

class ScheduledJobWorker
  @queue = :scheduled_jobs

  def self.perform(job)
    Rails.logger.info "Executing: #{job}"
    system(job)
  end
end

In lib/clock.rb, you can schedule however many task you’d like. Clockwork offers fine-grained control over scheduling, which is nicely documented on their Readme.

require 'clockwork'
require File.expand_path('../../config/boot',        __FILE__)
require File.expand_path('../../config/environment', __FILE__)

include Clockwork

handler do |job|
  Rails.logger.info "Enqueuing job: '#{job}'"
  Resque.enqueue(ScheduledJobWorker, job)
end

every(1.hour, 'rake [your hourly rake task]')
every(12.hours, 'rake [your twice-daily rake task]')

Next up, let’s configure Resque. Resque relies on Redis for maintaining its queue. Its default configuration will work locally but not when we deploy to Cloud Foundry. So, in config/initializers/resque.rb, we’ll add the following code:

begin
  redis_config = JSON.parse(ENV["VCAP_SERVICES"])["rediscloud-n/a"].first["credentials"]
  Resque.redis = "redis://:#{redis_config["password"]}@#{redis_config["hostname"]}:#{redis_config["port"]}"
rescue
  Rails.logger.info "Did not detect a Cloud Foundry redis configuration. Using the Resque's default configuration"
end

…and in lib/tasks/resque.rake, we add one line of code which makes resque commands available as rake tasks.

require 'resque/tasks'

 

Deploying your new apps

Sweet!! We’re ready to deploy to Cloud Foundry now. Deploy the resque application as below:

$ cf push yourapp-resque --command "bundle exec 'QUEUE=* rake environment resque:work'"
Instances> 1

1: 128M
2: 256M
3: 512M
4: 1G
Memory Limit> 256M

Creating yourapp-resque... OK

1: yourapp-resque
2: none
Subdomain> 2

1: cfapps.io
2: none
Domain> 2

Create services for application?> n #if you don't have Redis already, enter 'y' here and set it up.

Bind other services to application?> y

1: yourapp-newrelic
2: yourapp-sendgrid
3: yourapp-redis
Which service?> 3

Binding yourapp-redis to yourapp-resque... OK
Bind another service?> n # You'll actually select 'y', and bind your resque app to each service the real app is using.

Save configuration?> n

You also need to copy whatever environment variables your jobs need in order to run, from your main application to the Resque application. This might include things like secret keys, authorization codes, external service URLs. You can view the environment variables for an app using ‘cf env’

$ cf env yourapp

and set them using ‘cf set-env’

$ cf set-env yourapp-resque SOME_KEY some_value --no-restart

The –no-restart option makes it faster to set each variable. After you’ve set all the environment variables needed for resque, restart your Resque app.

One last step – deploy your scheduling app like below, also binding it to each service the actual app is using.

$ cf push yourapp-clock --command 'bundle exec clockwork lib/clock.rb'

Finally, check your logs to make sure everything is working properly. If you run cf logs yourapp-clock, you should see “Enqueuing job: …”. Similarly, your resque app’s logs should show “Executing:…” without any subsequent errors.

Voila! You’ve now got a fully functional scheduler!

[Thanks to Travis Grathwell for helping out with this blog post!]