Middle Management manages your Delayed Job workers on Heroku. It “hires” and “fires” workers automatically so that you get all of your work done quickly for as little money as possible.

It requires delayed_job 2.1.2 or greater.

To get started:

1) Add “middle_management” to your Gemfile and run “bundle install”

2) Set the following REQUIRED environment variables using heroku config:add KEY=value:

MIDDLE_MANAGEMENT_HEROKU_USERNAME (your heroku username)
MIDDLE_MANAGEMENT_HEROKU_PASSWORD (your heroku password)
MIDDLE_MANAGEMENT_HEROKU_APP (the name of your heroku app)
MIDDLE_MANAGEMENT_MIN_WORKERS (the minimum number of workers to keep running; typically 0)
MIDDLE_MANAGEMENT_MAX_WORKERS (the maximum number of workers to allow to run)

3) Deploy to heroku and voila - you’ve got auto-scaling workers!

You can also limit the rate at which workers will scale up by setting:

MIDDLE_MANAGEMENT_JOBS_PER_WORKER (default = 1)

This should be the number of jobs a single worker can do more quickly than it takes to bring up another heroku worker. For short tasks with high throughput (e.g. sending emails), you should bump this up (I set it at 10). For long tasks with low throughput (e.g. PDF generation or traversing a social graph), keep this low.

How it works:

Every time a job is created or destroyed, Middle Management checks to see how many jobs are outstanding and how many workers are reserved to work on them. If the number of workers needed differs from the number of current workers, Middle Management hires or fires the needed or excess workers.

workers_needed is calculated as: outstanding jobs / MIDDLE_MANAGEMENT_JOBS_PER_WORKER

Middle Management will ALWAYS honor the maximum number of workers, as set by the MIDDLE_MANAGEMENT_MAX_WORKERS variable, and the minimum number of workers, as set by the MIDDLE_MANAGEMENT_MIN_WORKERS variable. If you use Delayed Job to schedule future jobs, you should always keep at least one worker running so that it can be worked off at the appropriate time. If all of your jobs are runnable at the time they are created, it is safe to set MIDDLE_MANAGEMENT_MIN_WORKERS to zero.

Step-by-step chart, assumes: MIDDLE_MANAGEMENT_MIN_WORKERS=0 MIDDLE_MANAGEMENT_MAX_WORKERS=10 MIDDLE_MANAGEMENT_JOBS_PER_WORKER=1

App Action Middle Management Action Total Workers Running Boot Up 0 Rotate 3 images Start 3 workers 3 First image rotated Stop 1 worker 2 Second image rotated Stop 1 worker 1 Send 105 emails with send_later Start 8 workers 10 First 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent 10 Next 10 emails sent Stop 5 workers 5 Last 5 emails sent Stop 5 workers 0

Accuracy and Caching Notes:

  • Job counts are queried no more than once per minute. If you create and destroy jobs through ActiveRecord, the internal counts will always be accurate; however if you delete jobs from a SQL tool, the counts will be out of date until the next count query is made.

  • To prevent flooding Heroku with API calls when large batches of jobs are created, worker API calls are made no more than once every 10 seconds. If a call is algorithmically ready to be made before that, it will be delayed 10 seconds. In a worst case scenario, this could allow your workers to run 10 seconds longer than needed.

Testing in your heroku environment:

Middle Management comes with a Slacker class, whose sole purpose is to create jobs that take some time to run. You can use this class to create jobs in your production environment and monitor how your workers scale accordingly.

To get started, go to a heroku console and run: > 30.times MiddleManagement::Slacker.delay.slack_for(10)

This will create 30 jobs, each of which sleeps for 10 seconds. Middle Management will scale your workers as these jobs are created and run off. You can monitor the changes to your worker count by using “heroku workers” from your command line.

github.com/freerobby/heroku_mbo is an empty rails app that includes this gem. If you want to fiddle with this gem in isolation, I recommend cloning that repo, creating a heroku app from it, and playing with it on a console.