resque-rate_limited_queue 0.0.30

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b71e99ba935500ac4f784badf3fb036ac4cc9d78
4
+ data.tar.gz: 93fd4bc5c271b4b2761b8df4e133684d22671bfa
5
+ SHA512:
6
+ metadata.gz: ceb160532dbd82f4128b480ed324969a9368193ddecb1c3465a72c088adb798b9fe80ebb2c1ecd163f4cd8a93c308c0dfc482fcd2d4377a5b6360969da807f44
7
+ data.tar.gz: f71914524346ce7f3c8afd53be98f489d0a05ad29cc11f89130fff68084bdf6495f98b3305cda0089d9d24ad177746482e5158b3c3e1a27839839692525cf0f7
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in resque_rate_limited_queue.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 pavoni
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Resque Rate Limited Queue
2
+
3
+ PLEASE NOTE THIS IS STILL EXPERIMENTAL - I EXPECT TO RELEASE A STABLE VERSION IN EARLY JAN 2015
4
+
5
+ A Resque plugin which makes handling jobs that use rate limited apis easier
6
+
7
+ 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.
8
+
9
+ There are two ways to use the gem.
10
+
11
+ 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.
12
+
13
+ If you are using another API, then you need to write a little code that catches the rate limit signal.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'resque-rate-limited-queue'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install resque-rate-limited-queue
30
+
31
+ ## Usage
32
+
33
+ ### Configuration
34
+ #### Redis
35
+ The gem uses [redis-mutex](https://github.com/kenn/redis-mutex ) which requires you to register the Redis server: (e.g. in `config/initializers/redis_mutex.rb` for Rails)
36
+
37
+ ```ruby
38
+ RedisClassy.redis = Redis.new
39
+ ```
40
+ Note that Redis Mutex uses the `redis-classy` gem internally.
41
+
42
+ #### Un Pause
43
+ Queues can be unpaused in two ways.
44
+
45
+ The most elegant is using [resque-scheduler](https://github.com/resque/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.
46
+
47
+ To tell the gem to use resque-scheduler you need to include resque-scheduler 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.
48
+
49
+ ```ruby
50
+ Resque::Plugins::RateLimitedQueue::UnPause.queue = :my_queue
51
+ ```
52
+
53
+ 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.
54
+
55
+ #### Workers
56
+ 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:
57
+
58
+ Either
59
+ ```ruby
60
+ bin/resque work --queues=high,low,twitter_api
61
+ ```
62
+ or
63
+ ```ruby
64
+ env QUEUES=high,low,twitter_api bundle exec rake jobs:work
65
+ ```
66
+
67
+ NOT
68
+ ```ruby
69
+ bin/resque work --queues=*
70
+ ```
71
+ or NOT
72
+ ```ruby
73
+ env QUEUES=* bundle exec rake jobs:work
74
+ ```
75
+
76
+ #### Unpausing on heroku
77
+ 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:
78
+
79
+ ```ruby
80
+ Resque::Plugins::RateLimitedQueue.TwitterQueue.un_pause
81
+ Resque::Plugins::RateLimitedQueue.AngellistQueue.un_pause
82
+ MyQueue.un_pause
83
+ MyJob.un_pause
84
+ ```
85
+ ### A Pausable job using one of the build-in queues (Twitter, Angellist, Evernote)
86
+ If you're using the [twitter gem[ (https://github.com/sferik/twitter), this is really simple. Instead of queuing using Resque.enqueue, you just use Resque::Plugins::RateLimitedQueue:TwitterQueue.enqueue.
87
+
88
+ Make sure your code in perform doesn't catch the rate_limit exception.
89
+
90
+ 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
91
+
92
+ ```ruby
93
+ class TwitterJob
94
+ class << self
95
+ def refresh(handle)
96
+ Resque::Plugins::RateLimitedQueue:TwitterQueue.enqueue(TwitterJob, handle)
97
+ end
98
+
99
+ def perform(handle)
100
+ do_something_with_the_twitter_gem(handle)
101
+ end
102
+ end
103
+ end
104
+ ```
105
+
106
+ ### A single class of pausable job using a new api
107
+ If you only have one class of job you want to queue using the api, then you can use the PauseQueue module directly
108
+
109
+ ```ruby
110
+ class MyApiJob
111
+ extend Resque::Plugins::RateLimitedQueue
112
+ @queue = :my_api
113
+ WAIT_TIME = 60*60
114
+
115
+ def self.perform(*params)
116
+ do_api_stuff
117
+ rescue MyApiRateLimit
118
+ pause_for(Time.now + WAIT_TIME, name)
119
+ rate_limited_requeue(self, *params)
120
+ end
121
+
122
+ def self.enqueue(*params)
123
+ rate_limited_enqueue(self, *params)
124
+ end
125
+ end
126
+ ````
127
+
128
+ ### Multiple classes of pausable job using a new api
129
+ If you have more than one class of job you want to queue to the api, then you can need to add another Queue class. This isn't hard
130
+
131
+ ```ruby
132
+ class MyApiQueue < Resque::Plugins::RateLimitedQueue::BaseApiQueue
133
+ @queue = :my_api
134
+ WAIT_TIME = 60*60
135
+
136
+ def self.perform(klass, *params)
137
+ super
138
+ rescue MyApiRateLimit
139
+ pause_for(Time.now + WAIT_TIME, name)
140
+ rate_limited_requeue(self, klass, *params)
141
+ end
142
+ end
143
+ ````
144
+ If you do this - please contribute - and I'll add to the gem.
145
+
146
+ ## Contributing
147
+
148
+ 1. Fork it ( https://github.com/[my-github-username]/resque_rate_limited_queue/fork )
149
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
150
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
151
+ 4. Push to the branch (`git push origin my-new-feature`)
152
+ 5. Create a new Pull Request
153
+
154
+ ## Final thoughts
155
+ Thanks to [Dominic](https://github.com/dominicsayers) for idea of renaming the redis key - and the sample code that does this.
156
+
157
+ This is my first gem - so please forgive any errors - and feedback very welcome
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,3 @@
1
+ module RateLimitedQueue
2
+ VERSION = "0.0.30"
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'angellist_api'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module RateLimitedQueue
6
+ class AngellistQueue < BaseApiQueue
7
+ WAIT_TIME = 60
8
+ @queue = :angellist_api
9
+
10
+ def self.perform(klass, *params)
11
+ super
12
+ rescue AngellistApi::Error::TooManyRequests
13
+ pause_for(Time.now + (60 * 60))
14
+ rate_limited_requeue(self, klass, *params)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module Resque
2
+ module Plugins
3
+ module RateLimitedQueue
4
+ class BaseApiQueue
5
+ extend Resque::Plugins::RateLimitedQueue
6
+ def self.perform(klass, *params)
7
+ find_class(klass).perform(*params)
8
+ end
9
+
10
+ def self.enqueue(klass, *params)
11
+ rate_limited_enqueue(self, klass.to_s, *params)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ require 'evernote-thrift'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module RateLimitedQueue
6
+ class EvernoteQueue < BaseApiQueue
7
+ @queue = :evernote_api
8
+
9
+ def self.perform(klass, *params)
10
+ super
11
+ rescue Evernote::EDAM::Error::EDAMSystemException => e
12
+ pause_for(Time.now + 60 * e.rateLimitDuration.seconds)
13
+ rate_limited_requeue(self, klass, *params)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ require 'twitter'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module RateLimitedQueue
6
+ class TwitterQueue < BaseApiQueue
7
+ @queue = :twitter_api
8
+
9
+ def self.perform(klass, *params)
10
+ super
11
+ rescue Twitter::Error::TooManyRequests,
12
+ Twitter::Error::EnhanceYourCalm => e
13
+ pause_for(Time.now + e.rate_limit.reset_in)
14
+ rate_limited_requeue(self, klass, *params)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,80 @@
1
+ module Resque
2
+ module Plugins
3
+ module RateLimitedQueue
4
+ RESQUE_PREFIX = 'queue:'
5
+ MUTEX = 'Resque::Plugins::RateLimitedQueue'
6
+
7
+ def around_perform_with_check_and_requeue(*params)
8
+ paused = false
9
+ with_lock do
10
+ paused = paused?
11
+ Resque.enqueue_to(paused_queue_name, self, *params) if paused
12
+ end
13
+ return if paused
14
+ yield
15
+ end
16
+
17
+ def rate_limited_enqueue(klass, *params)
18
+ with_lock do
19
+ if paused?
20
+ Resque.enqueue_to(paused_queue_name, klass, *params)
21
+ else
22
+ Resque.enqueue_to(@queue, klass, *params)
23
+ end
24
+ end
25
+ end
26
+
27
+ def rate_limited_requeue(klass, *params)
28
+ # split from above to make it easy to stub for testing
29
+ rate_limited_enqueue(klass, *params)
30
+ end
31
+
32
+ def pause_for(timestamp)
33
+ pause
34
+ UnPause.enqueue(timestamp, name)
35
+ end
36
+
37
+ def un_pause
38
+ with_lock do
39
+ if paused?
40
+ begin
41
+ Resque.redis.renamenx(RESQUE_PREFIX + paused_queue_name, RESQUE_PREFIX + @queue.to_s)
42
+ rescue Redis::CommandError => e
43
+ raise unless e.message == 'ERR no such key'
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def pause
50
+ Resque.redis.renamenx(RESQUE_PREFIX + @queue.to_s, RESQUE_PREFIX + paused_queue_name)
51
+ rescue Redis::CommandError => e
52
+ raise unless e.message == 'ERR no such key'
53
+ end
54
+
55
+ def paused?
56
+ Resque.redis.exists(RESQUE_PREFIX + paused_queue_name)
57
+ end
58
+
59
+ def paused_queue_name
60
+ @queue.to_s + '_paused'
61
+ end
62
+
63
+ def with_lock
64
+ if Resque.inline
65
+ yield
66
+ else
67
+ RedisMutex.with_lock(MUTEX, block: 60, expire: 120) { yield }
68
+ end
69
+ end
70
+
71
+ def find_class(klass)
72
+ return klass if klass.is_a? Class
73
+ return Object.const_get(klass) unless klass.include?('::')
74
+ klass.split('::').reduce(Object) do |mod, class_name|
75
+ mod.const_get(class_name)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ module Resque
2
+ module Plugins
3
+ module RateLimitedQueue
4
+ class UnPause
5
+ @queue = nil
6
+
7
+ class << self
8
+ attr_writer(:queue)
9
+
10
+ def use?
11
+ Resque.respond_to?(:enqueue_at_with_queue) && @queue
12
+ end
13
+
14
+ def enqueue(timestamp, klass)
15
+ # If Resque scheduler is installed and queue is set - use it to queue a wake up job
16
+ return unless use?
17
+ Resque.enqueue_at_with_queue(
18
+ @queue,
19
+ timestamp,
20
+ Resque::Plugins::RateLimitedQueue::UnPause,
21
+ klass)
22
+ end
23
+
24
+ def perform(klass)
25
+ class_from_string(klass.to_s).un_pause
26
+ end
27
+
28
+ def class_from_string(str)
29
+ str.split('::').reduce(Object) do |mod, class_name|
30
+ mod.const_get(class_name)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ require 'resque'
2
+ require 'redis-mutex'
3
+ require 'resque/version'
4
+ require 'resque/plugins/rate_limited/rate_limited_queue'
5
+ require 'resque/plugins/rate_limited/rate_limited_un_pause'
6
+ require 'resque/plugins/rate_limited/apis/base_api_queue'
7
+ require 'resque/plugins/rate_limited/apis/angellist_queue'
8
+ require 'resque/plugins/rate_limited/apis/evernote_queue'
9
+ require 'resque/plugins/rate_limited/apis/twitter_queue'
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque-rate_limited_queue/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "resque-rate_limited_queue"
8
+ spec.version = RateLimitedQueue::VERSION
9
+ spec.authors = ["Greg Dowling"]
10
+ spec.email = ["mail@greddowling.com"]
11
+ spec.summary = %q{A Resque plugin to help manage jobs that use rate limited apis, pausing when you hit the limits and restarting later.}
12
+ spec.description = %q{A Resque plugin which allows you to create dedicated queues for jobs that use rate limited apis.
13
+ These queues will pause when one of the jobs hits a rate limit, and unpause after a suitable time period.
14
+ The rate_limited_queue can be used directly, and just requires catching the rate limit exception and pausing the
15
+ queue. There are also additional queues provided that already include the pause/rety logic for twitter, angelist
16
+ and evernote; these allow you to support rate limited apis with minimal changes.}
17
+
18
+ spec.homepage = ""
19
+ spec.license = "MIT"
20
+
21
+ spec.files = `git ls-files -z`.split("\x0")
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency('resque', '~> 1.9', '>= 1.9.10')
27
+ spec.add_dependency('redis-mutex','~> 4.0', '>= 4.0.0')
28
+
29
+ spec.add_dependency("angellist_api", '~> 1.0', '>= 1.0.7')
30
+ spec.add_dependency("evernote-thrift", '~> 1.25', '>= 1.25.1')
31
+ spec.add_dependency("twitter", '~> 5.11', '>= 5.11.0')
32
+
33
+
34
+ spec.add_development_dependency("bundler", "~> 1.7")
35
+ spec.add_development_dependency("rake", "~> 10.0")
36
+ spec.add_development_dependency("rspec", "~> 2.6")
37
+ spec.add_development_dependency("simplecov", '~> 0.9.1')
38
+
39
+
40
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+ require 'resque_rate_limited_queue'
3
+
4
+ class RateLimitedTestQueueAL
5
+ def self.perform(succeed)
6
+ raise(AngellistApi::Error::TooManyRequests, 'error') unless succeed
7
+ end
8
+ end
9
+
10
+ describe Resque::Plugins::RateLimitedQueue::AngellistQueue do
11
+ describe 'enqueue' do
12
+ it 'enqueues to the correct queue with the correct parameters' do
13
+ Resque.should_receive(:enqueue_to).with(
14
+ :angellist_api,
15
+ Resque::Plugins::RateLimitedQueue::AngellistQueue,
16
+ RateLimitedTestQueueAL.to_s,
17
+ true)
18
+ Resque::Plugins::RateLimitedQueue::AngellistQueue
19
+ .enqueue(RateLimitedTestQueueAL, true)
20
+ end
21
+ end
22
+
23
+ describe 'perform' do
24
+ before do
25
+ Resque.inline = true
26
+ end
27
+ context 'with everything' do
28
+ it 'calls the class with the right parameters' do
29
+ RateLimitedTestQueueAL.should_receive(:perform).with('test_param')
30
+ Resque::Plugins::RateLimitedQueue::AngellistQueue
31
+ .enqueue(RateLimitedTestQueueAL, 'test_param')
32
+ end
33
+ end
34
+
35
+ context 'with rate limit exception' do
36
+ before do
37
+ Resque::Plugins::RateLimitedQueue::AngellistQueue.stub(:rate_limited_requeue)
38
+ end
39
+ it 'pauses queue when request fails' do
40
+ Resque::Plugins::RateLimitedQueue::AngellistQueue.should_receive(:pause_for)
41
+ Resque::Plugins::RateLimitedQueue::AngellistQueue
42
+ .enqueue(RateLimitedTestQueueAL, false)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require 'resque_rate_limited_queue'
3
+
4
+ class RateLimitDuration
5
+ def self.seconds
6
+ 60
7
+ end
8
+ end
9
+
10
+ class RateLimitedTestQueueEn
11
+ def self.perform(succeed)
12
+ raise(Evernote::EDAM::Error::EDAMSystemException, rateLimitDuration: RateLimitDuration) unless succeed
13
+ end
14
+ end
15
+
16
+ describe Resque::Plugins::RateLimitedQueue::EvernoteQueue do
17
+ describe 'enqueue' do
18
+ it 'enqueues to the correct queue with the correct parameters' do
19
+ Resque.should_receive(:enqueue_to).with(
20
+ :evernote_api,
21
+ Resque::Plugins::RateLimitedQueue::EvernoteQueue,
22
+ RateLimitedTestQueueEn.to_s,
23
+ true)
24
+ Resque::Plugins::RateLimitedQueue::EvernoteQueue
25
+ .enqueue(RateLimitedTestQueueEn, true)
26
+ end
27
+ end
28
+
29
+ describe 'perform' do
30
+ before do
31
+ Resque.inline = true
32
+ end
33
+ context 'with everything' do
34
+ it 'calls the class with the right parameters' do
35
+ RateLimitedTestQueueEn.should_receive(:perform).with('test_param')
36
+ Resque::Plugins::RateLimitedQueue::EvernoteQueue
37
+ .enqueue(RateLimitedTestQueueEn, 'test_param')
38
+ end
39
+ end
40
+
41
+ context 'with rate limit exception' do
42
+ before do
43
+ Resque::Plugins::RateLimitedQueue::EvernoteQueue.stub(:rate_limited_requeue)
44
+ end
45
+ it 'pauses queue when request fails' do
46
+ Resque::Plugins::RateLimitedQueue::EvernoteQueue.should_receive(:pause_for)
47
+ Resque::Plugins::RateLimitedQueue::EvernoteQueue
48
+ .enqueue(RateLimitedTestQueueEn, false)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'resque_rate_limited_queue'
3
+
4
+ class RateLimitedTestQueueTw
5
+ def self.perform(succeed)
6
+ raise(Twitter::Error::TooManyRequests
7
+ .new('', 'x-rate-limit-reset' => (Time.now + 60).to_i)) unless succeed
8
+ end
9
+ end
10
+
11
+ describe Resque::Plugins::RateLimitedQueue::TwitterQueue do
12
+ describe 'enqueue' do
13
+ it 'enqueues to the correct queue with the correct parameters' do
14
+ Resque.should_receive(:enqueue_to).with(
15
+ :twitter_api,
16
+ Resque::Plugins::RateLimitedQueue::TwitterQueue,
17
+ RateLimitedTestQueueTw.to_s,
18
+ true)
19
+ Resque::Plugins::RateLimitedQueue::TwitterQueue
20
+ .enqueue(RateLimitedTestQueueTw, true)
21
+ end
22
+ end
23
+
24
+ describe 'perform' do
25
+ before do
26
+ Resque.inline = true
27
+ end
28
+ context 'with everything' do
29
+ it 'calls the class with the right parameters' do
30
+ RateLimitedTestQueueTw.should_receive(:perform).with('test_param')
31
+ Resque::Plugins::RateLimitedQueue::TwitterQueue
32
+ .enqueue(RateLimitedTestQueueTw, 'test_param')
33
+ end
34
+ end
35
+
36
+ context 'with rate limit exception' do
37
+ before do
38
+ Resque::Plugins::RateLimitedQueue::TwitterQueue.stub(:rate_limited_requeue)
39
+ end
40
+ it 'pauses queue when request fails' do
41
+ Resque::Plugins::RateLimitedQueue::TwitterQueue.should_receive(:pause_for)
42
+ Resque::Plugins::RateLimitedQueue::TwitterQueue
43
+ .enqueue(RateLimitedTestQueueTw, false)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,177 @@
1
+ require 'spec_helper'
2
+ require 'resque_rate_limited_queue'
3
+
4
+ class RateLimitedTestQueue
5
+ extend Resque::Plugins::RateLimitedQueue
6
+
7
+ @queue = :test
8
+
9
+ def self.perform(succeed)
10
+ rate_limited_requeue(self, succeed) unless succeed
11
+ end
12
+
13
+ def self.queue_name
14
+ @queue.to_s
15
+ end
16
+
17
+ def self.queue
18
+ @queue
19
+ end
20
+ end
21
+
22
+ describe Resque::Plugins::RateLimitedQueue do
23
+ it 'should be compliance with Resque::Plugin document' do
24
+ expect { Resque::Plugin.lint(Resque::Plugins::RateLimitedQueue) }.to_not raise_error
25
+ end
26
+
27
+ shared_examples_for 'queue' do |queue_suffix|
28
+ it 'should queue to the correct queue' do
29
+ queue_param = queue_suffix.empty? ? RateLimitedTestQueue.queue : "#{RateLimitedTestQueue.queue_name}#{queue_suffix}"
30
+ Resque.should_receive(:enqueue_to).with(queue_param, nil, nil)
31
+ RateLimitedTestQueue.rate_limited_enqueue(nil, nil)
32
+ end
33
+ end
34
+
35
+ context 'when queue is not paused' do
36
+ before do
37
+ RateLimitedTestQueue.stub(:paused?).and_return(false)
38
+ end
39
+
40
+ describe 'enqueue' do
41
+ include_examples 'queue', ''
42
+ end
43
+
44
+ describe 'paused?' do
45
+ it { RateLimitedTestQueue.paused?.should be false }
46
+ end
47
+
48
+ describe 'perform' do
49
+ it 'should requeue the job on failure' do
50
+ Resque.should_receive(:enqueue_to)
51
+ RateLimitedTestQueue.perform(false)
52
+ end
53
+
54
+ it 'should not requeue the job on success' do
55
+ Resque.should_not_receive(:enqueue_to)
56
+ RateLimitedTestQueue.perform(true)
57
+ end
58
+
59
+ end
60
+
61
+ describe 'pause' do
62
+ it 'should rename the queue to paused' do
63
+ Resque.redis.should_receive(:renamenx).with("queue:#{RateLimitedTestQueue.queue_name}", "queue:#{RateLimitedTestQueue.queue_name}_paused")
64
+ RateLimitedTestQueue.pause
65
+ end
66
+ end
67
+
68
+ describe 'un_pause' do
69
+ it 'should not unpause the queue' do
70
+ Resque.redis.should_not_receive(:renamenx).with("queue:#{RateLimitedTestQueue.queue_name}", "queue:#{RateLimitedTestQueue.queue_name}_paused")
71
+ RateLimitedTestQueue.un_pause
72
+ end
73
+ end
74
+
75
+ describe 'pause_for' do
76
+ before do
77
+ Resque.redis.stub(:renamenx).and_return(true)
78
+ end
79
+
80
+ it 'should pause the queue' do
81
+ RateLimitedTestQueue.should_receive(:pause)
82
+ RateLimitedTestQueue.pause_for(Time.now + (5 * 60 * 60))
83
+ end
84
+
85
+ it 'should schedule an unpause job' do
86
+ Resque::Plugins::RateLimitedQueue::UnPause.should_receive(:enqueue)
87
+ .with(nil, 'RateLimitedTestQueue')
88
+ RateLimitedTestQueue.pause_for(nil)
89
+ end
90
+ end
91
+ end
92
+
93
+ context 'when queue is paused' do
94
+ before do
95
+ RateLimitedTestQueue.stub(:paused?).and_return(true)
96
+ end
97
+
98
+ describe 'enqueue' do
99
+ include_examples 'queue', '_paused'
100
+ end
101
+
102
+ describe 'paused?' do
103
+ it { RateLimitedTestQueue.paused?.should be true }
104
+ end
105
+
106
+ describe 'perform' do
107
+ it 'should not execute the block' do
108
+ Resque.should_receive(:enqueue_to).with("#{RateLimitedTestQueue.queue_name}_paused", RateLimitedTestQueue, true)
109
+ RateLimitedTestQueue.should_not_receive(:perform)
110
+ RateLimitedTestQueue.around_perform_with_check_and_requeue(true)
111
+ end
112
+ end
113
+
114
+ describe 'un_pause' do
115
+ it 'should rename the queue to live' do
116
+ Resque.redis.should_receive(:renamenx).with("queue:#{RateLimitedTestQueue.queue_name}_paused", "queue:#{RateLimitedTestQueue.queue_name}")
117
+ RateLimitedTestQueue.un_pause
118
+ end
119
+ end
120
+ end
121
+
122
+ describe 'find_class' do
123
+ it 'works with symbol' do
124
+ RateLimitedTestQueue.find_class(RateLimitedTestQueue).should eq RateLimitedTestQueue
125
+ end
126
+
127
+ it 'works with simple string' do
128
+ RateLimitedTestQueue.find_class('RateLimitedTestQueue').should eq RateLimitedTestQueue
129
+ end
130
+
131
+ it 'works with complex string' do
132
+ RateLimitedTestQueue.find_class('Resque::Plugins::RateLimitedQueue').should eq Resque::Plugins::RateLimitedQueue
133
+ end
134
+ end
135
+
136
+ context 'with redis errors' do
137
+ before do
138
+ RateLimitedTestQueue.stub(:paused?).and_return(true)
139
+ end
140
+ context 'with not found error' do
141
+ before do
142
+ Resque.redis.stub(:renamenx).and_raise(Redis::CommandError.new('ERR no such key'))
143
+ end
144
+
145
+ describe 'pause' do
146
+ it 'should not throw exception' do
147
+ expect { RateLimitedTestQueue.pause }.to_not raise_error
148
+ end
149
+ end
150
+
151
+ describe 'un_pause' do
152
+ it 'should not throw exception' do
153
+ expect { RateLimitedTestQueue.un_pause }.to_not raise_error
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+ context 'with other errror' do
160
+ before do
161
+ Resque.redis.stub(:renamenx).and_raise(Redis::CommandError.new('ERR something else'))
162
+ end
163
+
164
+ describe 'pause' do
165
+ it 'should throw exception' do
166
+ expect { RateLimitedTestQueue.pause }.to raise_error(Redis::CommandError)
167
+ end
168
+ end
169
+
170
+ describe 'un_pause' do
171
+ it 'should throw exception' do
172
+ expect { RateLimitedTestQueue.un_pause }.to raise_error(Redis::CommandError)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'resque_rate_limited_queue'
3
+
4
+ class RateLimitedTestQueue
5
+ end
6
+
7
+ describe Resque::Plugins::RateLimitedQueue::UnPause do
8
+ describe 'perform' do
9
+ it 'unpauses the queue' do
10
+ RateLimitedTestQueue.should_receive(:un_pause)
11
+ Resque::Plugins::RateLimitedQueue::UnPause.perform(RateLimitedTestQueue)
12
+ end
13
+ end
14
+
15
+ describe 'enqueue' do
16
+ before { Resque.stub(:respond_to?).and_return(true) }
17
+ context 'with no queue defined' do
18
+ it 'does not queue the job' do
19
+ Resque.should_not_receive(:enqueue_at_with_queue)
20
+ Resque::Plugins::RateLimitedQueue::UnPause.enqueue(Time.now, RateLimitedTestQueue)
21
+ end
22
+ end
23
+
24
+ context 'with queue defined' do
25
+ before { Resque::Plugins::RateLimitedQueue::UnPause.queue = :queue_name }
26
+ it 'queues the job' do
27
+ Resque.should_receive(:enqueue_at_with_queue).with(
28
+ :queue_name,
29
+ nil,
30
+ Resque::Plugins::RateLimitedQueue::UnPause,
31
+ RateLimitedTestQueue)
32
+
33
+ Resque::Plugins::RateLimitedQueue::UnPause.enqueue(nil, RateLimitedTestQueue)
34
+ end
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rspec'
5
+ require 'redis-classy'
6
+ require 'redis-mutex'
7
+
8
+ require 'simplecov'
9
+
10
+ SimpleCov.start
11
+
12
+ RSpec.configure do |config|
13
+ # Use database 15 for testing so we don't accidentally step on your real data.
14
+ RedisClassy.redis = Redis.new(db: 15)
15
+ unless RedisClassy.keys.empty?
16
+ abort '[ERROR]: Redis database 15 not empty! If you are sure, run "rake flushdb" beforehand.'
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,233 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-rate_limited_queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.30
5
+ platform: ruby
6
+ authors:
7
+ - Greg Dowling
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: resque
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.9.10
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.9'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.9.10
33
+ - !ruby/object:Gem::Dependency
34
+ name: redis-mutex
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 4.0.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '4.0'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 4.0.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: angellist_api
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.0'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 1.0.7
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 1.0.7
73
+ - !ruby/object:Gem::Dependency
74
+ name: evernote-thrift
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '1.25'
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.25.1
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.25'
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 1.25.1
93
+ - !ruby/object:Gem::Dependency
94
+ name: twitter
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '5.11'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 5.11.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '5.11'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 5.11.0
113
+ - !ruby/object:Gem::Dependency
114
+ name: bundler
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.7'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '1.7'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rake
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '10.0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '10.0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: rspec
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '2.6'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '2.6'
155
+ - !ruby/object:Gem::Dependency
156
+ name: simplecov
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: 0.9.1
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: 0.9.1
169
+ description: |-
170
+ A Resque plugin which allows you to create dedicated queues for jobs that use rate limited apis.
171
+ These queues will pause when one of the jobs hits a rate limit, and unpause after a suitable time period.
172
+ The rate_limited_queue can be used directly, and just requires catching the rate limit exception and pausing the
173
+ queue. There are also additional queues provided that already include the pause/rety logic for twitter, angelist
174
+ and evernote; these allow you to support rate limited apis with minimal changes.
175
+ email:
176
+ - mail@greddowling.com
177
+ executables: []
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - ".gitignore"
182
+ - ".rspec"
183
+ - Gemfile
184
+ - LICENSE.txt
185
+ - README.md
186
+ - Rakefile
187
+ - lib/resque-rate_limited_queue/version.rb
188
+ - lib/resque/plugins/rate_limited/apis/angellist_queue.rb
189
+ - lib/resque/plugins/rate_limited/apis/base_api_queue.rb
190
+ - lib/resque/plugins/rate_limited/apis/evernote_queue.rb
191
+ - lib/resque/plugins/rate_limited/apis/twitter_queue.rb
192
+ - lib/resque/plugins/rate_limited/rate_limited_queue.rb
193
+ - lib/resque/plugins/rate_limited/rate_limited_un_pause.rb
194
+ - lib/resque/rate_limited_queue.rb
195
+ - resque-rate_limited_queue.gemspec
196
+ - spec/apis/angellist_queue_spec.rb
197
+ - spec/apis/evernote_queue_spec.rb
198
+ - spec/apis/twitter_queue_spec.rb
199
+ - spec/rate_limited_queue_spec.rb
200
+ - spec/rate_limited_un_pause_spec.rb
201
+ - spec/spec_helper.rb
202
+ homepage: ''
203
+ licenses:
204
+ - MIT
205
+ metadata: {}
206
+ post_install_message:
207
+ rdoc_options: []
208
+ require_paths:
209
+ - lib
210
+ required_ruby_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ required_rubygems_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ">="
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ requirements: []
221
+ rubyforge_project:
222
+ rubygems_version: 2.4.2
223
+ signing_key:
224
+ specification_version: 4
225
+ summary: A Resque plugin to help manage jobs that use rate limited apis, pausing when
226
+ you hit the limits and restarting later.
227
+ test_files:
228
+ - spec/apis/angellist_queue_spec.rb
229
+ - spec/apis/evernote_queue_spec.rb
230
+ - spec/apis/twitter_queue_spec.rb
231
+ - spec/rate_limited_queue_spec.rb
232
+ - spec/rate_limited_un_pause_spec.rb
233
+ - spec/spec_helper.rb