No commit activity in last 3 years
No release in over 3 years
A Resque plugin which allows you to create dedicated queues for jobs that use rate-limited APIs. These queues will pause when one of the jobs hits a rate limit, and unpause after a suitable time period. The rate-limited queue can be used directly, and just requires catching the rate limit exception and pausing the queue. There are also additional queues provided that already include the pause/retry logic for Twitter, AngelList and Evernote; these allow you to support rate-limited APIs with minimal changes.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 1.0.7, ~> 1.0
>= 1.25.1, ~> 1.25
>= 4.0.0, ~> 4.0
>= 1.9.10, ~> 1.9
>= 5.11.0
 Project Readme

Resque rate-limited

Gem version Gem downloads Build Status Code Climate Test Coverage Dependency Status Security

A Resque plugin which makes handling jobs that use rate-limited APIs easier

If you have a series of jobs in a queue, this gem will pause the queue when one of the jobs hits a rate limit, and re-start the queue when the rate limit has expired.

There are two ways to use the gem:

  1. If the API you are using has a dedicated queue included in the gem (currently Twitter, Angellist and Evernote) then you just need to make some very minor changes to how you queue jobs, and the gem will do the rest.
  2. If you are using another API, then you need to write a little code that catches the rate limit signal.

Installation

Add this line to your application's Gemfile:

gem 'resque-rate_limited'

And then execute:

$ bundle

Or install it yourself as:

$ gem install resque-rate_limited

Usage

Redis

The gem uses redis-mutex which requires you to register the Redis server: (e.g. in config/initializers/redis_mutex.rb for Rails)

RedisClassy.redis = Redis.new

Note that Redis Mutex uses the redis-classy gem internally.

Un Pause

Queues can be unpaused in two ways.

The most elegant is using resque-scheduler, this works well as long as you aren't running on a platform like heroku which requires a dedicated worker to run the resque-scheduler.

To tell the gem to use resque-scheduler you need to include it in your Gemfile - and also let the gem know which queue to use to schedule the unpause job (make sure this isn't a queue that could get paused). Put this in an initializer.

Resque::Plugins::RateLimited::UnPause.queue = :my_queue

Please see the section below on how to unpause on heroku as an alternative. If you don't install resque-scheduler AND configure the queue, then the gem will not schedule unpause jobs this way.

Workers

Queues are paused by renaming them, so a resque queue called twitter_api will be renamed twitter_api_paused when it hits a rate limit. Of course this will only work if your resque workers are not also taking jobs from the twitter_api_paused queue. So your worker commands need to look like:

Either

bin/resque work --queues=high,low,twitter_api

or

env QUEUES=high,low,twitter_api bundle exec rake jobs:work

NOT

bin/resque work --queues=*

and NOT

env QUEUES=* bundle exec rake jobs:work

Unpausing on heroku

The built in schedler on heroku doesn't support dynamic scheduling from an API, so unless you want to provision an extra worker to run resque-scheduler - the best option is just to unpause all your queues on a regular basis. If they aren't paused this is a harmless no-op. If not enough time has elapsed the jobs will just hit the rate_limit and get paused again. We've found that a hourly rake unpause job seems to work well. The rake task would need to call:

Resque::Plugins::RateLimited.TwitterQueue.un_pause
Resque::Plugins::RateLimited.AngellistQueue.un_pause
MyQueue.un_pause
MyJob.un_pause

A pausable job using one of the build-in queues (Twitter, Angellist, Evernote)

If you're using the twitter gem, this is really simple. Instead of queuing using Resque.enqueue, you just use Resque::Plugins::RateLimited:TwitterQueue.enqueue.

Make sure your code in perform doesn't catch the rate limit exception.

The TwitterQueue will catch the exception and pause the queue (as well as re-scheduling the jobs and scheduling an un pause (if you are using resque-scheduler)). Any jobs you add while the queue is paused will be added to the paused queue

class TwitterJob
  class << self
    def refresh(handle)
      Resque::Plugins::RateLimited:TwitterQueue.enqueue(TwitterJob, handle)
    end

    def perform(handle)
      do_something_with_the_twitter_gem(handle)
    end
  end
end

A single class of pausable job using a new API

If you only have one class of job you want to queue using the API, then you can use the PauseQueue module directly

class MyApiJob
  extend Resque::Plugins::RateLimited
  @queue = :my_api
  WAIT_TIME = 60*60

  def self.perform(*params)
    do_api_stuff
  rescue MyApiRateLimit
    pause_until(Time.now + WAIT_TIME, name)
    rate_limited_requeue(self, *params)
  end

  def self.enqueue(*params)
    rate_limited_enqueue(self, *params)
  end
end

Multiple classes of pausable job using a new API

If you have more than one class of job you want to queue to the API, then you need to add another Queue class. This isn't hard

class MyApiQueue < Resque::Plugins::RateLimited::BaseApiQueue
  @queue = :my_api
  WAIT_TIME = 60*60

  def self.perform(klass, *params)
    super
  rescue MyApiRateLimit
    pause_until(Time.now + WAIT_TIME, name)
    rate_limited_requeue(self, klass, *params)
  end
end

If you do this - please contribute - and I'll add to the gem.

Development Documentation

All the functions are class methods

rate_limited_enqueue(klass, *params)
rate_limited_requeue(klass, *params)

Queue the job specified to the resque queue specified by @queue. rate_limited_requeue is intended for use when you need the job to be pushed back to the queue; there are two reasons to split this from rate_limited_enqueue. Firstly it makes testing with stubs easier - secondly there is a boundary condition when you need to requeue the last job in the queue.

pause

Pauses the queue specified by @queue, if it is not already paused. In most cases you should call pause_until to pause a queue when you hit a rate limit.

un_pause

Un-pauses the queue specified by @queue, if it is paused.

pause_until(timestamp)

Pauses the queue (specified by @queue) and then queues a job to unpause the queue specified by @queue, using resque-scheduler to the queue specified by Resque::Plugins::RateLimited::UnPause.queue at the timestamp specified. If resque-schedule is not included, or UnPause.queue isn't specified this will just pause the queue.

This is the prefered function to call when you hit a rate limit, since it with work regardless of the unpause method used by the application.

paused?

This returns true or false to indicate wheher the queue is paused. Be aware that the queue state could change get after the call returns, but before your code executes. Use with_lock if you need to avoid this.

paused_queue_name

Returns the name of the queue when it is paused.

with_lock(&block)

Takes ownership of the PauseQueue semaphor before executing the block passed. Useful if you need to test the state of the queue and take some action without the state changing.

find_class(klass)

Takes the parameter passed, and if it's a string class name, tries to turn it into a class.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/resque_rate_limited/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Final thoughts

Thanks to Dominic for idea of renaming the redis key - and the sample code that does this.

This is my first gem - so please forgive any errors - and feedback very welcome