sneakers_custom_bunny 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG.md +20 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +8 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +172 -0
  9. data/ROADMAP.md +18 -0
  10. data/Rakefile +11 -0
  11. data/bin/sneakers +5 -0
  12. data/examples/benchmark_worker.rb +20 -0
  13. data/examples/max_retry_handler.rb +78 -0
  14. data/examples/metrics_worker.rb +28 -0
  15. data/examples/newrelic_metrics_worker.rb +40 -0
  16. data/examples/profiling_worker.rb +69 -0
  17. data/examples/sneakers.conf.rb.example +11 -0
  18. data/examples/title_scraper.rb +23 -0
  19. data/examples/workflow_worker.rb +23 -0
  20. data/lib/sneakers.rb +83 -0
  21. data/lib/sneakers/cli.rb +115 -0
  22. data/lib/sneakers/concerns/logging.rb +34 -0
  23. data/lib/sneakers/concerns/metrics.rb +34 -0
  24. data/lib/sneakers/configuration.rb +59 -0
  25. data/lib/sneakers/handlers/maxretry.rb +191 -0
  26. data/lib/sneakers/handlers/oneshot.rb +30 -0
  27. data/lib/sneakers/metrics/logging_metrics.rb +16 -0
  28. data/lib/sneakers/metrics/newrelic_metrics.rb +37 -0
  29. data/lib/sneakers/metrics/null_metrics.rb +13 -0
  30. data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
  31. data/lib/sneakers/publisher.rb +34 -0
  32. data/lib/sneakers/queue.rb +65 -0
  33. data/lib/sneakers/runner.rb +82 -0
  34. data/lib/sneakers/spawner.rb +27 -0
  35. data/lib/sneakers/support/production_formatter.rb +11 -0
  36. data/lib/sneakers/support/utils.rb +18 -0
  37. data/lib/sneakers/tasks.rb +34 -0
  38. data/lib/sneakers/version.rb +3 -0
  39. data/lib/sneakers/worker.rb +151 -0
  40. data/lib/sneakers/workergroup.rb +47 -0
  41. data/sneakers.gemspec +35 -0
  42. data/spec/fixtures/require_worker.rb +17 -0
  43. data/spec/sneakers/cli_spec.rb +63 -0
  44. data/spec/sneakers/concerns/logging_spec.rb +39 -0
  45. data/spec/sneakers/concerns/metrics_spec.rb +38 -0
  46. data/spec/sneakers/configuration_spec.rb +75 -0
  47. data/spec/sneakers/publisher_spec.rb +83 -0
  48. data/spec/sneakers/queue_spec.rb +115 -0
  49. data/spec/sneakers/runner_spec.rb +26 -0
  50. data/spec/sneakers/sneakers_spec.rb +75 -0
  51. data/spec/sneakers/support/utils_spec.rb +44 -0
  52. data/spec/sneakers/worker_handlers_spec.rb +390 -0
  53. data/spec/sneakers/worker_spec.rb +463 -0
  54. data/spec/spec_helper.rb +13 -0
  55. 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