sneakers_custom_bunny 1.0.4
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +3 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +172 -0
- data/ROADMAP.md +18 -0
- data/Rakefile +11 -0
- data/bin/sneakers +5 -0
- data/examples/benchmark_worker.rb +20 -0
- data/examples/max_retry_handler.rb +78 -0
- data/examples/metrics_worker.rb +28 -0
- data/examples/newrelic_metrics_worker.rb +40 -0
- data/examples/profiling_worker.rb +69 -0
- data/examples/sneakers.conf.rb.example +11 -0
- data/examples/title_scraper.rb +23 -0
- data/examples/workflow_worker.rb +23 -0
- data/lib/sneakers.rb +83 -0
- data/lib/sneakers/cli.rb +115 -0
- data/lib/sneakers/concerns/logging.rb +34 -0
- data/lib/sneakers/concerns/metrics.rb +34 -0
- data/lib/sneakers/configuration.rb +59 -0
- data/lib/sneakers/handlers/maxretry.rb +191 -0
- data/lib/sneakers/handlers/oneshot.rb +30 -0
- data/lib/sneakers/metrics/logging_metrics.rb +16 -0
- data/lib/sneakers/metrics/newrelic_metrics.rb +37 -0
- data/lib/sneakers/metrics/null_metrics.rb +13 -0
- data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
- data/lib/sneakers/publisher.rb +34 -0
- data/lib/sneakers/queue.rb +65 -0
- data/lib/sneakers/runner.rb +82 -0
- data/lib/sneakers/spawner.rb +27 -0
- data/lib/sneakers/support/production_formatter.rb +11 -0
- data/lib/sneakers/support/utils.rb +18 -0
- data/lib/sneakers/tasks.rb +34 -0
- data/lib/sneakers/version.rb +3 -0
- data/lib/sneakers/worker.rb +151 -0
- data/lib/sneakers/workergroup.rb +47 -0
- data/sneakers.gemspec +35 -0
- data/spec/fixtures/require_worker.rb +17 -0
- data/spec/sneakers/cli_spec.rb +63 -0
- data/spec/sneakers/concerns/logging_spec.rb +39 -0
- data/spec/sneakers/concerns/metrics_spec.rb +38 -0
- data/spec/sneakers/configuration_spec.rb +75 -0
- data/spec/sneakers/publisher_spec.rb +83 -0
- data/spec/sneakers/queue_spec.rb +115 -0
- data/spec/sneakers/runner_spec.rb +26 -0
- data/spec/sneakers/sneakers_spec.rb +75 -0
- data/spec/sneakers/support/utils_spec.rb +44 -0
- data/spec/sneakers/worker_handlers_spec.rb +390 -0
- data/spec/sneakers/worker_spec.rb +463 -0
- data/spec/spec_helper.rb +13 -0
- metadata +306 -0
@@ -0,0 +1,390 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sneakers'
|
3
|
+
require 'timeout'
|
4
|
+
require 'sneakers/handlers/oneshot'
|
5
|
+
require 'sneakers/handlers/maxretry'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
|
9
|
+
# Specific tests of the Handler implementations you can use to deal with job
|
10
|
+
# results. These tests only make sense with a worker that requires acking.
|
11
|
+
|
12
|
+
class HandlerTestWorker
|
13
|
+
include Sneakers::Worker
|
14
|
+
from_queue 'defaults',
|
15
|
+
:ack => true
|
16
|
+
|
17
|
+
def work(msg)
|
18
|
+
if msg.is_a?(StandardError)
|
19
|
+
raise msg
|
20
|
+
elsif msg.is_a?(String)
|
21
|
+
hash = maybe_json(msg)
|
22
|
+
if hash.is_a?(Hash)
|
23
|
+
hash['response'].to_sym
|
24
|
+
else
|
25
|
+
hash
|
26
|
+
end
|
27
|
+
else
|
28
|
+
msg
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def maybe_json(string)
|
33
|
+
JSON.parse(string)
|
34
|
+
rescue
|
35
|
+
string
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class TestPool
|
40
|
+
def process(*args,&block)
|
41
|
+
block.call
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
describe 'Handlers' do
|
47
|
+
let(:channel) { Object.new }
|
48
|
+
let(:queue) { Object.new }
|
49
|
+
let(:worker) { HandlerTestWorker.new(@queue, TestPool.new) }
|
50
|
+
|
51
|
+
before(:each) do
|
52
|
+
Sneakers.configure(:daemonize => true, :log => 'sneakers.log')
|
53
|
+
Sneakers::Worker.configure_logger(Logger.new('/dev/null'))
|
54
|
+
Sneakers::Worker.configure_metrics
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'Oneshot' do
|
58
|
+
before(:each) do
|
59
|
+
@opts = Object.new
|
60
|
+
@handler = Sneakers::Handlers::Oneshot.new(channel, queue, @opts)
|
61
|
+
|
62
|
+
@header = Object.new
|
63
|
+
stub(@header).delivery_tag { 37 }
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#do_work' do
|
67
|
+
it 'should work and handle acks' do
|
68
|
+
mock(channel).acknowledge(37, false)
|
69
|
+
|
70
|
+
worker.do_work(@header, nil, :ack, @handler)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should work and handle rejects' do
|
74
|
+
mock(channel).reject(37, false)
|
75
|
+
|
76
|
+
worker.do_work(@header, nil, :reject, @handler)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should work and handle requeues' do
|
80
|
+
mock(channel).reject(37, true)
|
81
|
+
|
82
|
+
worker.do_work(@header, nil, :requeue, @handler)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should work and handle user-land timeouts' do
|
86
|
+
mock(channel).reject(37, false)
|
87
|
+
|
88
|
+
worker.do_work(@header, nil, :timeout, @handler)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should work and handle user-land error' do
|
92
|
+
mock(channel).reject(37, false)
|
93
|
+
|
94
|
+
worker.do_work(@header, nil, StandardError.new('boom!'), @handler)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should work and handle noops' do
|
98
|
+
worker.do_work(@header, nil, :wait, @handler)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'Maxretry' do
|
105
|
+
let(:max_retries) { nil }
|
106
|
+
let(:props_with_x_death_count) {
|
107
|
+
{
|
108
|
+
:headers => {
|
109
|
+
"x-death" => [
|
110
|
+
{
|
111
|
+
"count" => 3,
|
112
|
+
"reason" => "expired",
|
113
|
+
"queue" => "downloads-retry",
|
114
|
+
"time" => Time.now,
|
115
|
+
"exchange" => "RawMail-retry",
|
116
|
+
"routing-keys" => ["RawMail"]
|
117
|
+
},
|
118
|
+
{
|
119
|
+
"count" => 3,
|
120
|
+
"reason" => "rejected",
|
121
|
+
"queue" => "downloads",
|
122
|
+
"time" => Time.now,
|
123
|
+
"exchange" => "",
|
124
|
+
"routing-keys" => ["RawMail"]
|
125
|
+
}
|
126
|
+
]
|
127
|
+
},
|
128
|
+
:delivery_mode => 1
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
before(:each) do
|
133
|
+
@opts = {
|
134
|
+
:exchange => 'sneakers',
|
135
|
+
:durable => 'true',
|
136
|
+
}.tap do |opts|
|
137
|
+
opts[:retry_max_times] = max_retries unless max_retries.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
mock(queue).name { 'downloads' }
|
141
|
+
|
142
|
+
@retry_exchange = Object.new
|
143
|
+
@error_exchange = Object.new
|
144
|
+
@requeue_exchange = Object.new
|
145
|
+
|
146
|
+
@retry_queue = Object.new
|
147
|
+
@error_queue = Object.new
|
148
|
+
|
149
|
+
mock(channel).exchange('downloads-retry',
|
150
|
+
:type => 'topic',
|
151
|
+
:durable => 'true').once { @retry_exchange }
|
152
|
+
mock(channel).exchange('downloads-error',
|
153
|
+
:type => 'topic',
|
154
|
+
:durable => 'true').once { @error_exchange }
|
155
|
+
mock(channel).exchange('downloads-retry-requeue',
|
156
|
+
:type => 'topic',
|
157
|
+
:durable => 'true').once { @requeue_exchange }
|
158
|
+
|
159
|
+
mock(channel).queue('downloads-retry',
|
160
|
+
:durable => 'true',
|
161
|
+
:arguments => {
|
162
|
+
:'x-dead-letter-exchange' => 'downloads-retry-requeue',
|
163
|
+
:'x-message-ttl' => 60000
|
164
|
+
}
|
165
|
+
).once { @retry_queue }
|
166
|
+
mock(@retry_queue).bind(@retry_exchange, :routing_key => '#')
|
167
|
+
|
168
|
+
mock(channel).queue('downloads-error',
|
169
|
+
:durable => 'true').once { @error_queue }
|
170
|
+
mock(@error_queue).bind(@error_exchange, :routing_key => '#')
|
171
|
+
|
172
|
+
mock(queue).bind(@requeue_exchange, :routing_key => '#')
|
173
|
+
|
174
|
+
@handler = Sneakers::Handlers::Maxretry.new(channel, queue, @opts)
|
175
|
+
|
176
|
+
@header = Object.new
|
177
|
+
stub(@header).delivery_tag { 37 }
|
178
|
+
|
179
|
+
@props = {}
|
180
|
+
@props_with_x_death = {
|
181
|
+
:headers => {
|
182
|
+
"x-death" => [
|
183
|
+
{
|
184
|
+
"reason" => "expired",
|
185
|
+
"queue" => "downloads-retry",
|
186
|
+
"time" => Time.now,
|
187
|
+
"exchange" => "RawMail-retry",
|
188
|
+
"routing-keys" => ["RawMail"]
|
189
|
+
},
|
190
|
+
{
|
191
|
+
"reason" => "rejected",
|
192
|
+
"queue" => "downloads",
|
193
|
+
"time" => Time.now,
|
194
|
+
"exchange" => "",
|
195
|
+
"routing-keys" => ["RawMail"]
|
196
|
+
}
|
197
|
+
]
|
198
|
+
},
|
199
|
+
:delivery_mode => 1}
|
200
|
+
end
|
201
|
+
|
202
|
+
# it 'allows overriding the retry exchange name'
|
203
|
+
# it 'allows overriding the error exchange name'
|
204
|
+
# it 'allows overriding the retry timeout'
|
205
|
+
|
206
|
+
describe '#do_work' do
|
207
|
+
before do
|
208
|
+
@now = Time.now
|
209
|
+
end
|
210
|
+
|
211
|
+
# Used to stub out the publish method args. Sadly RR doesn't support
|
212
|
+
# this, only proxying existing methods.
|
213
|
+
module MockPublish
|
214
|
+
attr_reader :data, :opts, :called
|
215
|
+
|
216
|
+
def publish(data, opts)
|
217
|
+
@data = data
|
218
|
+
@opts = opts
|
219
|
+
@called = true
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'should work and handle acks' do
|
224
|
+
mock(channel).acknowledge(37, false)
|
225
|
+
|
226
|
+
worker.do_work(@header, @props, :ack, @handler)
|
227
|
+
end
|
228
|
+
|
229
|
+
describe 'rejects' do
|
230
|
+
describe 'more retries ahead' do
|
231
|
+
it 'should work and handle rejects' do
|
232
|
+
mock(channel).reject(37, false)
|
233
|
+
|
234
|
+
worker.do_work(@header, @props_with_x_death, :reject, @handler)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe 'no more retries' do
|
239
|
+
let(:max_retries) { 1 }
|
240
|
+
|
241
|
+
it 'sends the rejection to the error queue' do
|
242
|
+
mock(@header).routing_key { '#' }
|
243
|
+
mock(channel).acknowledge(37, false)
|
244
|
+
|
245
|
+
@error_exchange.extend MockPublish
|
246
|
+
worker.do_work(@header, @props_with_x_death, :reject, @handler)
|
247
|
+
@error_exchange.called.must_equal(true)
|
248
|
+
@error_exchange.opts.must_equal({ :routing_key => '#' })
|
249
|
+
data = JSON.parse(@error_exchange.data)
|
250
|
+
data['error'].must_equal('reject')
|
251
|
+
data['num_attempts'].must_equal(2)
|
252
|
+
data['payload'].must_equal(Base64.encode64(:reject.to_s))
|
253
|
+
Time.parse(data['failed_at']).wont_be_nil
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'counts the number of attempts using the count key' do
|
257
|
+
mock(@header).routing_key { '#' }
|
258
|
+
mock(channel).acknowledge(37, false)
|
259
|
+
|
260
|
+
@error_exchange.extend MockPublish
|
261
|
+
worker.do_work(@header, props_with_x_death_count, :reject, @handler)
|
262
|
+
@error_exchange.called.must_equal(true)
|
263
|
+
@error_exchange.opts.must_equal({ :routing_key => '#' })
|
264
|
+
data = JSON.parse(@error_exchange.data)
|
265
|
+
data['error'].must_equal('reject')
|
266
|
+
data['num_attempts'].must_equal(4)
|
267
|
+
data['payload'].must_equal(Base64.encode64(:reject.to_s))
|
268
|
+
Time.parse(data['failed_at']).wont_be_nil
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
describe 'requeues' do
|
275
|
+
it 'should work and handle requeues' do
|
276
|
+
mock(channel).reject(37, true)
|
277
|
+
|
278
|
+
worker.do_work(@header, @props_with_x_death, :requeue, @handler)
|
279
|
+
end
|
280
|
+
|
281
|
+
describe 'no more retries left' do
|
282
|
+
let(:max_retries) { 1 }
|
283
|
+
|
284
|
+
it 'continues to reject with requeue' do
|
285
|
+
mock(channel).reject(37, true)
|
286
|
+
|
287
|
+
worker.do_work(@header, @props_with_x_death, :requeue, @handler)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
describe 'timeouts' do
|
294
|
+
describe 'more retries ahead' do
|
295
|
+
it 'should reject the message' do
|
296
|
+
mock(channel).reject(37, false)
|
297
|
+
|
298
|
+
worker.do_work(@header, @props_with_x_death, :timeout, @handler)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
describe 'no more retries left' do
|
303
|
+
let(:max_retries) { 1 }
|
304
|
+
|
305
|
+
it 'sends the rejection to the error queue' do
|
306
|
+
mock(@header).routing_key { '#' }
|
307
|
+
mock(channel).acknowledge(37, false)
|
308
|
+
@error_exchange.extend MockPublish
|
309
|
+
|
310
|
+
worker.do_work(@header, @props_with_x_death, :timeout, @handler)
|
311
|
+
@error_exchange.called.must_equal(true)
|
312
|
+
@error_exchange.opts.must_equal({ :routing_key => '#' })
|
313
|
+
data = JSON.parse(@error_exchange.data)
|
314
|
+
data['error'].must_equal('timeout')
|
315
|
+
data['num_attempts'].must_equal(2)
|
316
|
+
data['payload'].must_equal(Base64.encode64(:timeout.to_s))
|
317
|
+
Time.parse(data['failed_at']).wont_be_nil
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe 'exceptions' do
|
323
|
+
describe 'more retries ahead' do
|
324
|
+
it 'should reject the message' do
|
325
|
+
mock(channel).reject(37, false)
|
326
|
+
|
327
|
+
worker.do_work(@header, @props_with_x_death, StandardError.new('boom!'), @handler)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
describe 'no more retries left' do
|
332
|
+
let(:max_retries) { 1 }
|
333
|
+
|
334
|
+
it 'sends the rejection to the error queue' do
|
335
|
+
mock(@header).routing_key { '#' }
|
336
|
+
mock(channel).acknowledge(37, false)
|
337
|
+
@error_exchange.extend MockPublish
|
338
|
+
|
339
|
+
worker.do_work(@header, @props_with_x_death, StandardError.new('boom!'), @handler)
|
340
|
+
@error_exchange.called.must_equal(true)
|
341
|
+
@error_exchange.opts.must_equal({ :routing_key => '#' })
|
342
|
+
data = JSON.parse(@error_exchange.data)
|
343
|
+
data['error'].must_equal('boom!')
|
344
|
+
data['error_class'].must_equal(StandardError.to_s)
|
345
|
+
data['backtrace'].wont_be_nil
|
346
|
+
data['num_attempts'].must_equal(2)
|
347
|
+
data['payload'].must_equal(Base64.encode64('boom!'))
|
348
|
+
Time.parse(data['failed_at']).wont_be_nil
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'should work and handle user-land error' do
|
354
|
+
mock(channel).reject(37, false)
|
355
|
+
|
356
|
+
worker.do_work(@header, @props, StandardError.new('boom!'), @handler)
|
357
|
+
end
|
358
|
+
|
359
|
+
it 'should work and handle noops' do
|
360
|
+
worker.do_work(@header, @props, :wait, @handler)
|
361
|
+
end
|
362
|
+
|
363
|
+
# Since we encode in json, we want to make sure if the actual payload is
|
364
|
+
# json, then it's something you can get back out.
|
365
|
+
describe 'JSON payloads' do
|
366
|
+
let(:max_retries) { 1 }
|
367
|
+
|
368
|
+
it 'properly encodes the json payload' do
|
369
|
+
mock(@header).routing_key { '#' }
|
370
|
+
mock(channel).acknowledge(37, false)
|
371
|
+
@error_exchange.extend MockPublish
|
372
|
+
|
373
|
+
payload = {
|
374
|
+
data: 'hello',
|
375
|
+
response: :timeout
|
376
|
+
}
|
377
|
+
worker.do_work(@header, @props_with_x_death, payload.to_json, @handler)
|
378
|
+
@error_exchange.called.must_equal(true)
|
379
|
+
@error_exchange.opts.must_equal({ :routing_key => '#' })
|
380
|
+
data = JSON.parse(@error_exchange.data)
|
381
|
+
data['error'].must_equal('timeout')
|
382
|
+
data['num_attempts'].must_equal(2)
|
383
|
+
data['payload'].must_equal(Base64.encode64(payload.to_json))
|
384
|
+
end
|
385
|
+
|
386
|
+
end
|
387
|
+
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
@@ -0,0 +1,463 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sneakers'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
|
6
|
+
class DummyWorker
|
7
|
+
include Sneakers::Worker
|
8
|
+
from_queue 'downloads',
|
9
|
+
:durable => false,
|
10
|
+
:ack => false,
|
11
|
+
:threads => 50,
|
12
|
+
:prefetch => 40,
|
13
|
+
:timeout_job_after => 1,
|
14
|
+
:exchange => 'dummy',
|
15
|
+
:heartbeat => 5
|
16
|
+
|
17
|
+
def work(msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class DefaultsWorker
|
22
|
+
include Sneakers::Worker
|
23
|
+
from_queue 'defaults'
|
24
|
+
|
25
|
+
def work(msg)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class TimeoutWorker
|
30
|
+
include Sneakers::Worker
|
31
|
+
from_queue 'defaults',
|
32
|
+
:timeout_job_after => 0.5,
|
33
|
+
:ack => true
|
34
|
+
|
35
|
+
def work(msg)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class AcksWorker
|
40
|
+
include Sneakers::Worker
|
41
|
+
from_queue 'defaults',
|
42
|
+
:ack => true
|
43
|
+
|
44
|
+
def work(msg)
|
45
|
+
if msg == :ack
|
46
|
+
ack!
|
47
|
+
elsif msg == :nack
|
48
|
+
nack!
|
49
|
+
elsif msg == :reject
|
50
|
+
reject!
|
51
|
+
else
|
52
|
+
msg
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class PublishingWorker
|
58
|
+
include Sneakers::Worker
|
59
|
+
from_queue 'defaults',
|
60
|
+
:ack => false,
|
61
|
+
:exchange => 'foochange'
|
62
|
+
|
63
|
+
def work(msg)
|
64
|
+
publish msg, :to_queue => 'target'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
class LoggingWorker
|
71
|
+
include Sneakers::Worker
|
72
|
+
from_queue 'defaults',
|
73
|
+
:ack => false
|
74
|
+
|
75
|
+
def work(msg)
|
76
|
+
logger.info "hello"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class MetricsWorker
|
82
|
+
include Sneakers::Worker
|
83
|
+
from_queue 'defaults',
|
84
|
+
:ack => true,
|
85
|
+
:timeout_job_after => 0.5
|
86
|
+
|
87
|
+
def work(msg)
|
88
|
+
metrics.increment "foobar"
|
89
|
+
msg
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class WithParamsWorker
|
94
|
+
include Sneakers::Worker
|
95
|
+
from_queue 'defaults',
|
96
|
+
:ack => true,
|
97
|
+
:timeout_job_after => 0.5
|
98
|
+
|
99
|
+
def work_with_params(msg, delivery_info, metadata)
|
100
|
+
msg
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
class TestPool
|
106
|
+
def process(*args,&block)
|
107
|
+
block.call
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def with_test_queuefactory(ctx, ack=true, msg=nil, nowork=false)
|
112
|
+
qf = Object.new
|
113
|
+
q = Object.new
|
114
|
+
s = Object.new
|
115
|
+
hdr = Object.new
|
116
|
+
mock(qf).build_queue(anything, anything, anything) { q }
|
117
|
+
mock(q).subscribe(anything){ s }
|
118
|
+
|
119
|
+
mock(s).each(anything) { |h,b| b.call(hdr, msg) unless nowork }
|
120
|
+
mock(hdr).ack{true} if !nowork && ack
|
121
|
+
mock(hdr).reject{true} if !nowork && !ack
|
122
|
+
|
123
|
+
mock(ctx).queue_factory { qf } # should return our own
|
124
|
+
end
|
125
|
+
|
126
|
+
describe Sneakers::Worker do
|
127
|
+
before do
|
128
|
+
@queue = Object.new
|
129
|
+
@exchange = Object.new
|
130
|
+
stub(@queue).name { 'test-queue' }
|
131
|
+
stub(@queue).opts { {} }
|
132
|
+
stub(@queue).exchange { @exchange }
|
133
|
+
|
134
|
+
Sneakers.clear!
|
135
|
+
Sneakers.configure(:daemonize => true, :log => 'sneakers.log')
|
136
|
+
Sneakers::Worker.configure_metrics
|
137
|
+
end
|
138
|
+
|
139
|
+
describe ".enqueue" do
|
140
|
+
it "publishes a message to the class queue" do
|
141
|
+
message = "my message"
|
142
|
+
mock = MiniTest::Mock.new
|
143
|
+
|
144
|
+
mock.expect(:publish, true) do |msg, opts|
|
145
|
+
msg.must_equal(message)
|
146
|
+
opts.must_equal(:to_queue => "defaults")
|
147
|
+
end
|
148
|
+
|
149
|
+
stub(Sneakers::Publisher).new { mock }
|
150
|
+
DefaultsWorker.enqueue(message)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "#initialize" do
|
155
|
+
describe "builds an internal queue" do
|
156
|
+
before do
|
157
|
+
@dummy_q = DummyWorker.new.queue
|
158
|
+
@defaults_q = DefaultsWorker.new.queue
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should build a queue with correct configuration given defaults" do
|
162
|
+
@defaults_q.name.must_equal('defaults')
|
163
|
+
@defaults_q.opts.to_hash.must_equal(
|
164
|
+
:runner_config_file => nil,
|
165
|
+
:metrics => nil,
|
166
|
+
:daemonize => true,
|
167
|
+
:start_worker_delay => 0.2,
|
168
|
+
:workers => 4,
|
169
|
+
:log => "sneakers.log",
|
170
|
+
:pid_path => "sneakers.pid",
|
171
|
+
:timeout_job_after => 5,
|
172
|
+
:prefetch => 10,
|
173
|
+
:threads => 10,
|
174
|
+
:durable => true,
|
175
|
+
:ack => true,
|
176
|
+
:amqp => "amqp://guest:guest@localhost:5672",
|
177
|
+
:vhost => "/",
|
178
|
+
:exchange => "sneakers",
|
179
|
+
:exchange_type => :direct,
|
180
|
+
:hooks => {},
|
181
|
+
:handler => Sneakers::Handlers::Oneshot,
|
182
|
+
:heartbeat => 2
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should build a queue with given configuration" do
|
187
|
+
@dummy_q.name.must_equal('downloads')
|
188
|
+
@dummy_q.opts.to_hash.must_equal(
|
189
|
+
:runner_config_file => nil,
|
190
|
+
:metrics => nil,
|
191
|
+
:daemonize => true,
|
192
|
+
:start_worker_delay => 0.2,
|
193
|
+
:workers => 4,
|
194
|
+
:log => "sneakers.log",
|
195
|
+
:pid_path => "sneakers.pid",
|
196
|
+
:timeout_job_after => 1,
|
197
|
+
:prefetch => 40,
|
198
|
+
:threads => 50,
|
199
|
+
:durable => false,
|
200
|
+
:ack => false,
|
201
|
+
:amqp => "amqp://guest:guest@localhost:5672",
|
202
|
+
:vhost => "/",
|
203
|
+
:exchange => "dummy",
|
204
|
+
:exchange_type => :direct,
|
205
|
+
:hooks => {},
|
206
|
+
:handler => Sneakers::Handlers::Oneshot,
|
207
|
+
:heartbeat => 5
|
208
|
+
)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "initializes worker" do
|
213
|
+
it "should generate a worker id" do
|
214
|
+
DummyWorker.new.id.must_match(/^worker-/)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
describe "#run" do
|
221
|
+
it "should subscribe on internal queue" do
|
222
|
+
q = Object.new
|
223
|
+
w = DummyWorker.new(q)
|
224
|
+
mock(q).subscribe(w).once #XXX once?
|
225
|
+
stub(q).name{ "test" }
|
226
|
+
stub(q).opts { nil }
|
227
|
+
w.run
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "#stop" do
|
232
|
+
it "should unsubscribe from internal queue" do
|
233
|
+
q = Object.new
|
234
|
+
mock(q).unsubscribe.once #XXX once?
|
235
|
+
stub(q).name { 'test-queue' }
|
236
|
+
stub(q).opts {nil}
|
237
|
+
w = DummyWorker.new(q)
|
238
|
+
w.stop
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
describe "#do_work" do
|
244
|
+
it "should perform worker's work" do
|
245
|
+
w = DummyWorker.new(@queue, TestPool.new)
|
246
|
+
mock(w).work("msg").once
|
247
|
+
w.do_work(nil, nil, "msg", nil)
|
248
|
+
end
|
249
|
+
|
250
|
+
it "should catch runtime exceptions from a bad work" do
|
251
|
+
w = AcksWorker.new(@queue, TestPool.new)
|
252
|
+
mock(w).work("msg").once{ raise "foo" }
|
253
|
+
handler = Object.new
|
254
|
+
header = Object.new
|
255
|
+
mock(handler).error(header, nil, "msg", anything)
|
256
|
+
mock(w.logger).error(/unexpected error \[Exception error="foo" error_class=RuntimeError backtrace=.*/)
|
257
|
+
w.do_work(header, nil, "msg", handler)
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should log exceptions from workers" do
|
261
|
+
handler = Object.new
|
262
|
+
header = Object.new
|
263
|
+
w = AcksWorker.new(@queue, TestPool.new)
|
264
|
+
mock(w).work("msg").once{ raise "foo" }
|
265
|
+
mock(w.logger).error(/error="foo" error_class=RuntimeError backtrace=/)
|
266
|
+
mock(handler).error(header, nil, "msg", anything)
|
267
|
+
w.do_work(header, nil, "msg", handler)
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should timeout if a work takes too long" do
|
271
|
+
w = TimeoutWorker.new(@queue, TestPool.new)
|
272
|
+
stub(w).work("msg"){ sleep 10 }
|
273
|
+
|
274
|
+
handler = Object.new
|
275
|
+
header = Object.new
|
276
|
+
|
277
|
+
mock(handler).timeout(header, nil, "msg")
|
278
|
+
mock(w.logger).error(/timeout/)
|
279
|
+
|
280
|
+
w.do_work(header, nil, "msg", handler)
|
281
|
+
end
|
282
|
+
|
283
|
+
describe "with ack" do
|
284
|
+
before do
|
285
|
+
@delivery_info = Object.new
|
286
|
+
stub(@delivery_info).delivery_tag{ "tag" }
|
287
|
+
|
288
|
+
@worker = AcksWorker.new(@queue, TestPool.new)
|
289
|
+
end
|
290
|
+
|
291
|
+
it "should work and handle acks" do
|
292
|
+
handler = Object.new
|
293
|
+
mock(handler).acknowledge(@delivery_info, nil, :ack)
|
294
|
+
|
295
|
+
@worker.do_work(@delivery_info, nil, :ack, handler)
|
296
|
+
end
|
297
|
+
|
298
|
+
it "should work and handle rejects" do
|
299
|
+
handler = Object.new
|
300
|
+
mock(handler).reject(@delivery_info, nil, :reject)
|
301
|
+
|
302
|
+
@worker.do_work(@delivery_info, nil, :reject, handler)
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should work and handle requeues" do
|
306
|
+
handler = Object.new
|
307
|
+
mock(handler).reject(@delivery_info, nil, :requeue, true)
|
308
|
+
|
309
|
+
@worker.do_work(@delivery_info, nil, :requeue, handler)
|
310
|
+
end
|
311
|
+
|
312
|
+
it "should work and handle user-land timeouts" do
|
313
|
+
handler = Object.new
|
314
|
+
mock(handler).timeout(@delivery_info, nil, :timeout)
|
315
|
+
|
316
|
+
@worker.do_work(@delivery_info, nil, :timeout, handler)
|
317
|
+
end
|
318
|
+
|
319
|
+
it "should work and handle user-land error" do
|
320
|
+
handler = Object.new
|
321
|
+
mock(handler).error(@delivery_info, nil, :error, anything)
|
322
|
+
|
323
|
+
@worker.do_work(@delivery_info, nil, :error, handler)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe "without ack" do
|
328
|
+
it "should work and not care about acking if not ack" do
|
329
|
+
handler = Object.new
|
330
|
+
mock(handler).reject(anything).never
|
331
|
+
mock(handler).acknowledge(anything).never
|
332
|
+
|
333
|
+
w = DummyWorker.new(@queue, TestPool.new)
|
334
|
+
w.do_work(nil, nil, 'msg', handler)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
describe 'publish' do
|
341
|
+
it 'should be able to publish a message from working context' do
|
342
|
+
w = PublishingWorker.new(@queue, TestPool.new)
|
343
|
+
mock(@exchange).publish('msg', :routing_key => 'target').once
|
344
|
+
w.do_work(nil, nil, 'msg', nil)
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'should be able to publish arbitrary metadata' do
|
348
|
+
w = PublishingWorker.new(@queue, TestPool.new)
|
349
|
+
mock(@exchange).publish('msg', :routing_key => 'target', :expiration => 1).once
|
350
|
+
w.publish 'msg', :to_queue => 'target', :expiration => 1
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
|
355
|
+
describe 'Logging' do
|
356
|
+
it 'should be able to use the logging facilities' do
|
357
|
+
log = Logger.new('/dev/null')
|
358
|
+
mock(log).debug(anything).once
|
359
|
+
mock(log).info("hello").once
|
360
|
+
Sneakers::Worker.configure_logger(log)
|
361
|
+
|
362
|
+
w = LoggingWorker.new(@queue, TestPool.new)
|
363
|
+
w.do_work(nil,nil,'msg',nil)
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'has a helper to constuct log prefix values' do
|
367
|
+
w = DummyWorker.new(@queue, TestPool.new)
|
368
|
+
w.instance_variable_set(:@id, 'worker-id')
|
369
|
+
m = w.log_msg('foo')
|
370
|
+
w.log_msg('foo').must_match(/\[worker-id\]\[#<Thread:.*>\]\[test-queue\]\[\{\}\] foo/)
|
371
|
+
end
|
372
|
+
|
373
|
+
describe '#worker_error' do
|
374
|
+
it 'only logs backtraces if present' do
|
375
|
+
w = DummyWorker.new(@queue, TestPool.new)
|
376
|
+
mock(w.logger).error(/cuz \[Exception error="boom!" error_class=RuntimeError\]/)
|
377
|
+
w.worker_error('cuz', RuntimeError.new('boom!'))
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
383
|
+
|
384
|
+
describe 'Metrics' do
|
385
|
+
before do
|
386
|
+
@handler = Object.new
|
387
|
+
@header = Object.new
|
388
|
+
|
389
|
+
# We don't care how these are called, we're focusing on metrics here.
|
390
|
+
stub(@handler).acknowledge
|
391
|
+
stub(@handler).reject
|
392
|
+
stub(@handler).timeout
|
393
|
+
stub(@handler).error
|
394
|
+
stub(@handler).noop
|
395
|
+
|
396
|
+
@delivery_info = Object.new
|
397
|
+
stub(@delivery_info).delivery_tag { "tag" }
|
398
|
+
|
399
|
+
@w = MetricsWorker.new(@queue, TestPool.new)
|
400
|
+
mock(@w.metrics).increment("work.MetricsWorker.started").once
|
401
|
+
mock(@w.metrics).increment("work.MetricsWorker.ended").once
|
402
|
+
mock(@w.metrics).timing("work.MetricsWorker.time").yields.once
|
403
|
+
end
|
404
|
+
|
405
|
+
it 'should be able to meter acks' do
|
406
|
+
mock(@w.metrics).increment("foobar").once
|
407
|
+
mock(@w.metrics).increment("work.MetricsWorker.handled.ack").once
|
408
|
+
@w.do_work(@delivery_info, nil, :ack, @handler)
|
409
|
+
end
|
410
|
+
|
411
|
+
it 'should be able to meter rejects' do
|
412
|
+
mock(@w.metrics).increment("foobar").once
|
413
|
+
mock(@w.metrics).increment("work.MetricsWorker.handled.reject").once
|
414
|
+
@w.do_work(@header, nil, :reject, @handler)
|
415
|
+
end
|
416
|
+
|
417
|
+
it 'should be able to meter requeue' do
|
418
|
+
mock(@w.metrics).increment("foobar").once
|
419
|
+
mock(@w.metrics).increment("work.MetricsWorker.handled.requeue").once
|
420
|
+
@w.do_work(@header, nil, :requeue, @handler)
|
421
|
+
end
|
422
|
+
|
423
|
+
it 'should be able to meter errors' do
|
424
|
+
mock(@w.metrics).increment("work.MetricsWorker.handled.error").once
|
425
|
+
mock(@w).work('msg'){ raise :error }
|
426
|
+
@w.do_work(@delivery_info, nil, 'msg', @handler)
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'should be able to meter timeouts' do
|
430
|
+
mock(@w.metrics).increment("work.MetricsWorker.handled.timeout").once
|
431
|
+
mock(@w).work('msg'){ sleep 10 }
|
432
|
+
@w.do_work(@delivery_info, nil, 'msg', @handler)
|
433
|
+
end
|
434
|
+
|
435
|
+
it 'defaults to noop when no response is specified' do
|
436
|
+
mock(@w.metrics).increment("foobar").once
|
437
|
+
mock(@w.metrics).increment("work.MetricsWorker.handled.noop").once
|
438
|
+
@w.do_work(@header, nil, nil, @handler)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
|
443
|
+
|
444
|
+
describe 'With Params' do
|
445
|
+
before do
|
446
|
+
@props = { :foo => 1 }
|
447
|
+
@handler = Object.new
|
448
|
+
@header = Object.new
|
449
|
+
|
450
|
+
@delivery_info = Object.new
|
451
|
+
|
452
|
+
stub(@handler).noop(@delivery_info, {:foo => 1}, :ack)
|
453
|
+
|
454
|
+
@w = WithParamsWorker.new(@queue, TestPool.new)
|
455
|
+
mock(@w.metrics).timing("work.WithParamsWorker.time").yields.once
|
456
|
+
end
|
457
|
+
|
458
|
+
it 'should call work_with_params and not work' do
|
459
|
+
mock(@w).work_with_params(:ack, @delivery_info, {:foo => 1}).once
|
460
|
+
@w.do_work(@delivery_info, {:foo => 1 }, :ack, @handler)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|