resque-rate_limited 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ module Resque
2
+ module Plugins
3
+ module RateLimited
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::RateLimited::UnPause,
21
+ klass
22
+ )
23
+ end
24
+
25
+ def perform(klass)
26
+ class_from_string(klass.to_s).un_pause
27
+ end
28
+
29
+ def class_from_string(str)
30
+ return Object.const_get(str) unless str.include?('::')
31
+ str.split('::').reduce(Object) do |mod, class_name|
32
+ mod.const_get(class_name)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ require 'resque'
2
+ require 'redis-mutex'
3
+ require 'resque/version'
4
+ require 'resque/plugins/rate_limited/rate_limited'
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,43 @@
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/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'resque-rate_limited'
8
+ spec.version = RateLimited::VERSION
9
+ spec.authors = ['Greg Dowling']
10
+ spec.email = ['mail@greddowling.com']
11
+ spec.summary = 'A Resque plugin to help manage jobs that use rate limited apis, pausing when you hit the limits and restarting later.'
12
+ spec.description = '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 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 = 'http://github.com/Xenapto/resque-rate_limited'
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
+ spec.add_development_dependency 'rake', '~> 10'
34
+ spec.add_development_dependency 'rspec', '~> 2'
35
+ spec.add_development_dependency 'simplecov', '~> 0'
36
+ spec.add_development_dependency 'rubocop', '~> 0'
37
+ spec.add_development_dependency 'reek', '~> 4'
38
+ spec.add_development_dependency 'listen', '~> 3.0', '< 3.1' # Dependency of guard, 3.1 requires Ruby 2.2+
39
+ spec.add_development_dependency 'guard', '~> 2'
40
+ spec.add_development_dependency 'guard-rspec', '~> 4'
41
+ spec.add_development_dependency 'guard-rubocop', '~> 1'
42
+ spec.add_development_dependency 'gem-release', '~> 0'
43
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+ require 'resque/rate_limited'
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::RateLimited::AngellistQueue do
11
+ before do
12
+ Resque::Plugins::RateLimited::AngellistQueue.stub(:paused?).and_return(false)
13
+ end
14
+
15
+ describe 'enqueue' do
16
+ it 'enqueues to the correct queue with the correct parameters' do
17
+ Resque.should_receive(:enqueue_to).with(
18
+ :angellist_api,
19
+ Resque::Plugins::RateLimited::AngellistQueue,
20
+ RateLimitedTestQueueAL.to_s,
21
+ true
22
+ )
23
+ Resque::Plugins::RateLimited::AngellistQueue
24
+ .enqueue(RateLimitedTestQueueAL, true)
25
+ end
26
+ end
27
+
28
+ describe 'perform' do
29
+ before do
30
+ Resque.inline = true
31
+ end
32
+ context 'with everything' do
33
+ it 'calls the class with the right parameters' do
34
+ RateLimitedTestQueueAL.should_receive(:perform).with('test_param')
35
+ Resque::Plugins::RateLimited::AngellistQueue
36
+ .enqueue(RateLimitedTestQueueAL, 'test_param')
37
+ end
38
+ end
39
+
40
+ context 'with rate limit exception' do
41
+ before do
42
+ Resque::Plugins::RateLimited::AngellistQueue.stub(:rate_limited_requeue)
43
+ end
44
+ it 'pauses queue when request fails' do
45
+ Resque::Plugins::RateLimited::AngellistQueue.should_receive(:pause_until)
46
+ Resque::Plugins::RateLimited::AngellistQueue
47
+ .enqueue(RateLimitedTestQueueAL, false)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+ require 'resque/rate_limited'
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(
13
+ Evernote::EDAM::Error::EDAMSystemException,
14
+ errorCode: Evernote::EDAM::Error::EDAMErrorCode::RATE_LIMIT_REACHED,
15
+ rateLimitDuration: RateLimitDuration
16
+ ) unless succeed
17
+ end
18
+ end
19
+
20
+ class RateLimitedTestQueueOther
21
+ def self.perform
22
+ raise(Evernote::EDAM::Error::EDAMSystemException)
23
+ end
24
+ end
25
+
26
+ describe Resque::Plugins::RateLimited::EvernoteQueue do
27
+ before do
28
+ Resque::Plugins::RateLimited::EvernoteQueue.stub(:paused?).and_return(false)
29
+ end
30
+ describe 'enqueue' do
31
+ it 'enqueues to the correct queue with the correct parameters' do
32
+ Resque.should_receive(:enqueue_to).with(
33
+ :evernote_api,
34
+ Resque::Plugins::RateLimited::EvernoteQueue,
35
+ RateLimitedTestQueueEn.to_s,
36
+ true
37
+ )
38
+ Resque::Plugins::RateLimited::EvernoteQueue
39
+ .enqueue(RateLimitedTestQueueEn, true)
40
+ end
41
+ end
42
+
43
+ describe 'perform' do
44
+ before do
45
+ Resque.inline = true
46
+ end
47
+ context 'with everything' do
48
+ it 'calls the class with the right parameters' do
49
+ RateLimitedTestQueueEn.should_receive(:perform).with('test_param')
50
+ Resque::Plugins::RateLimited::EvernoteQueue
51
+ .enqueue(RateLimitedTestQueueEn, 'test_param')
52
+ end
53
+ end
54
+
55
+ context 'with rate limit exception' do
56
+ before do
57
+ Resque::Plugins::RateLimited::EvernoteQueue.stub(:rate_limited_requeue)
58
+ end
59
+ it 'pauses queue when request fails' do
60
+ Resque::Plugins::RateLimited::EvernoteQueue.should_receive(:pause_until)
61
+ Resque::Plugins::RateLimited::EvernoteQueue
62
+ .enqueue(RateLimitedTestQueueEn, false)
63
+ end
64
+ end
65
+
66
+ context 'with exception that is not rate limit' do
67
+ before do
68
+ Resque::Plugins::RateLimited::EvernoteQueue.stub(:rate_limited_requeue)
69
+ end
70
+ it 'raises the exception when request fails' do
71
+ expect do
72
+ Resque::Plugins::RateLimited::EvernoteQueue.enqueue(RateLimitedTestQueueOther)
73
+ end.to raise_error Evernote::EDAM::Error::EDAMSystemException
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'resque/rate_limited'
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::RateLimited::TwitterQueue do
12
+ before do
13
+ Resque::Plugins::RateLimited::TwitterQueue.stub(:paused?).and_return(false)
14
+ end
15
+
16
+ describe 'enqueue' do
17
+ it 'enqueues to the correct queue with the correct parameters' do
18
+ Resque.should_receive(:enqueue_to).with(
19
+ :twitter_api,
20
+ Resque::Plugins::RateLimited::TwitterQueue,
21
+ RateLimitedTestQueueTw.to_s,
22
+ true
23
+ )
24
+ Resque::Plugins::RateLimited::TwitterQueue
25
+ .enqueue(RateLimitedTestQueueTw, 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
+ RateLimitedTestQueueTw.should_receive(:perform).with('test_param')
36
+ Resque::Plugins::RateLimited::TwitterQueue
37
+ .enqueue(RateLimitedTestQueueTw, 'test_param')
38
+ end
39
+ end
40
+
41
+ context 'with rate limit exception' do
42
+ before do
43
+ Resque::Plugins::RateLimited::TwitterQueue.stub(:rate_limited_requeue)
44
+ end
45
+ it 'pauses queue when request fails' do
46
+ Resque::Plugins::RateLimited::TwitterQueue.should_receive(:pause_until)
47
+ Resque::Plugins::RateLimited::TwitterQueue
48
+ .enqueue(RateLimitedTestQueueTw, false)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,243 @@
1
+ require 'spec_helper'
2
+ require 'resque/rate_limited'
3
+
4
+ class RateLimitedTestQueue
5
+ extend Resque::Plugins::RateLimited
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_private
14
+ @queue.to_s
15
+ end
16
+
17
+ def self.queue_private
18
+ @queue
19
+ end
20
+ end
21
+
22
+ describe Resque::Plugins::RateLimited do
23
+ it 'should be compliance with Resque::Plugin document' do
24
+ expect { Resque::Plugin.lint(Resque::Plugins::RateLimited) }.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_private : "#{RateLimitedTestQueue.queue_name_private}#{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
+ end
59
+
60
+ describe 'pause' do
61
+ it 'should rename the queue to paused' do
62
+ Resque.redis.should_receive(:renamenx).with("queue:#{RateLimitedTestQueue.queue_name_private}", "queue:#{RateLimitedTestQueue.queue_name_private}_paused")
63
+ RateLimitedTestQueue.pause
64
+ end
65
+ end
66
+
67
+ describe 'un_pause' do
68
+ it 'should not unpause the queue' do
69
+ Resque.redis.should_not_receive(:renamenx).with("queue:#{RateLimitedTestQueue.queue_name_private}", "queue:#{RateLimitedTestQueue.queue_name_private}_paused")
70
+ RateLimitedTestQueue.un_pause
71
+ end
72
+ end
73
+
74
+ describe 'pause_until' do
75
+ before do
76
+ Resque.redis.stub(:renamenx).and_return(true)
77
+ end
78
+
79
+ it 'should pause the queue' do
80
+ RateLimitedTestQueue.should_receive(:pause)
81
+ RateLimitedTestQueue.pause_until(Time.now + (5 * 60 * 60))
82
+ end
83
+
84
+ it 'should schedule an unpause job' do
85
+ Resque::Plugins::RateLimited::UnPause.should_receive(:enqueue)
86
+ .with(nil, 'RateLimitedTestQueue')
87
+ RateLimitedTestQueue.pause_until(nil)
88
+ end
89
+ end
90
+ end
91
+
92
+ context 'when queue is paused' do
93
+ before do
94
+ RateLimitedTestQueue.stub(:paused?).and_return(true)
95
+ end
96
+
97
+ describe 'enqueue' do
98
+ include_examples 'queue', '_paused'
99
+ end
100
+
101
+ describe 'paused?' do
102
+ it { RateLimitedTestQueue.paused?.should be true }
103
+ end
104
+
105
+ describe 'perform' do
106
+ it 'should not execute the block' do
107
+ Resque.should_receive(:enqueue_to).with("#{RateLimitedTestQueue.queue_name_private}_paused", RateLimitedTestQueue, true)
108
+ RateLimitedTestQueue.should_not_receive(:perform)
109
+ RateLimitedTestQueue.around_perform_with_check_and_requeue(true)
110
+ end
111
+ end
112
+
113
+ describe 'un_pause' do
114
+ it 'should rename the queue to live' do
115
+ Resque.redis.should_receive(:renamenx).with("queue:#{RateLimitedTestQueue.queue_name_private}_paused", "queue:#{RateLimitedTestQueue.queue_name_private}")
116
+ RateLimitedTestQueue.un_pause
117
+ end
118
+ end
119
+ end
120
+
121
+ describe 'when queue is paused and Resque is in inline mode' do
122
+ let(:resque_prefix) { Resque::Plugins::RateLimited::RESQUE_PREFIX }
123
+ let(:queue) { resque_prefix + RateLimitedTestQueue.queue_name_private }
124
+ let(:paused_queue) { resque_prefix + RateLimitedTestQueue.paused_queue_name }
125
+
126
+ before do
127
+ Resque.redis.stub(:exists).with(queue).and_return(false)
128
+ Resque.redis.stub(:exists).with(paused_queue).and_return(true)
129
+ Resque.inline = true
130
+ end
131
+
132
+ after do
133
+ Resque.inline = false
134
+ end
135
+
136
+ it 'would be paused' do
137
+ expect(Resque.redis.exists(queue)).to eq false
138
+ expect(Resque.redis.exists(paused_queue)).to eq true
139
+ end
140
+
141
+ it 'says it is not paused' do
142
+ expect(RateLimitedTestQueue.paused?).to eq false
143
+ end
144
+
145
+ it 'performs the job' do
146
+ expect do
147
+ # Stack overflow unless handled
148
+ RateLimitedTestQueue.rate_limited_enqueue(RateLimitedTestQueue, true)
149
+ end.not_to raise_error
150
+ end
151
+ end
152
+
153
+ describe 'find_class' do
154
+ it 'works with symbol' do
155
+ RateLimitedTestQueue.find_class(RateLimitedTestQueue).should eq RateLimitedTestQueue
156
+ end
157
+
158
+ it 'works with simple string' do
159
+ RateLimitedTestQueue.find_class('RateLimitedTestQueue').should eq RateLimitedTestQueue
160
+ end
161
+
162
+ it 'works with complex string' do
163
+ RateLimitedTestQueue.find_class('Resque::Plugins::RateLimited').should eq Resque::Plugins::RateLimited
164
+ end
165
+ end
166
+
167
+ context 'with redis errors' do
168
+ before do
169
+ RateLimitedTestQueue.stub(:paused?).and_return(true)
170
+ end
171
+ context 'with not found error' do
172
+ before do
173
+ Resque.redis.stub(:renamenx).and_raise(Redis::CommandError.new('ERR no such key'))
174
+ end
175
+
176
+ describe 'pause' do
177
+ it 'should not throw exception' do
178
+ expect { RateLimitedTestQueue.pause }.to_not raise_error
179
+ end
180
+ end
181
+
182
+ describe 'un_pause' do
183
+ it 'should not throw exception' do
184
+ expect { RateLimitedTestQueue.un_pause }.to_not raise_error
185
+ end
186
+ end
187
+ end
188
+
189
+ context 'with other errror' do
190
+ before do
191
+ Resque.redis.stub(:renamenx).and_raise(Redis::CommandError.new('ERR something else'))
192
+ end
193
+
194
+ describe 'pause' do
195
+ it 'should throw exception' do
196
+ expect { RateLimitedTestQueue.pause }.to raise_error(Redis::CommandError)
197
+ end
198
+ end
199
+
200
+ describe 'un_pause' do
201
+ it 'should throw exception' do
202
+ expect { RateLimitedTestQueue.un_pause }.to raise_error(Redis::CommandError)
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ describe 'paused?' do
209
+ context 'with paused queue' do
210
+ before do
211
+ Resque.redis.stub(:exists).with("queue:#{RateLimitedTestQueue.queue_name_private}_paused").and_return(true)
212
+ Resque.redis.stub(:exists).with("queue:#{RateLimitedTestQueue.queue_name_private}").and_return(false)
213
+ end
214
+
215
+ it 'should return the true if the paused queue exists' do
216
+ expect(RateLimitedTestQueue.paused?).to eq(true)
217
+ end
218
+ end
219
+
220
+ context 'with un paused queue' do
221
+ before do
222
+ Resque.redis.stub(:exists).with("queue:#{RateLimitedTestQueue.queue_name_private}_paused").and_return(false)
223
+ Resque.redis.stub(:exists).with("queue:#{RateLimitedTestQueue.queue_name_private}").and_return(true)
224
+ end
225
+
226
+ it 'should return the false if the main queue exists exist' do
227
+ expect(RateLimitedTestQueue.paused?).to eq(false)
228
+ end
229
+ end
230
+
231
+ context 'with unknown queue state' do
232
+ before do
233
+ Resque.redis.stub(:exists).with("queue:#{RateLimitedTestQueue.queue_name_private}_paused").and_return(false)
234
+ Resque.redis.stub(:exists).with("queue:#{RateLimitedTestQueue.queue_name_private}").and_return(false)
235
+ end
236
+
237
+ it 'should return the default' do
238
+ expect(RateLimitedTestQueue.paused?(true)).to eq(true)
239
+ expect(RateLimitedTestQueue.paused?(false)).to eq(false)
240
+ end
241
+ end
242
+ end
243
+ end