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.
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