kicks 3.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +24 -0
  3. data/.gitignore +12 -0
  4. data/ChangeLog.md +142 -0
  5. data/Dockerfile +24 -0
  6. data/Dockerfile.slim +20 -0
  7. data/Gemfile +8 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +209 -0
  11. data/Rakefile +12 -0
  12. data/bin/sneakers +6 -0
  13. data/docker-compose.yml +24 -0
  14. data/examples/benchmark_worker.rb +22 -0
  15. data/examples/max_retry_handler.rb +68 -0
  16. data/examples/metrics_worker.rb +34 -0
  17. data/examples/middleware_worker.rb +36 -0
  18. data/examples/newrelic_metrics_worker.rb +40 -0
  19. data/examples/profiling_worker.rb +69 -0
  20. data/examples/sneakers.conf.rb.example +11 -0
  21. data/examples/title_scraper.rb +36 -0
  22. data/examples/workflow_worker.rb +23 -0
  23. data/kicks.gemspec +44 -0
  24. data/lib/sneakers/cli.rb +122 -0
  25. data/lib/sneakers/concerns/logging.rb +34 -0
  26. data/lib/sneakers/concerns/metrics.rb +34 -0
  27. data/lib/sneakers/configuration.rb +125 -0
  28. data/lib/sneakers/content_encoding.rb +47 -0
  29. data/lib/sneakers/content_type.rb +47 -0
  30. data/lib/sneakers/error_reporter.rb +33 -0
  31. data/lib/sneakers/errors.rb +2 -0
  32. data/lib/sneakers/handlers/maxretry.rb +219 -0
  33. data/lib/sneakers/handlers/oneshot.rb +26 -0
  34. data/lib/sneakers/metrics/logging_metrics.rb +16 -0
  35. data/lib/sneakers/metrics/newrelic_metrics.rb +32 -0
  36. data/lib/sneakers/metrics/null_metrics.rb +13 -0
  37. data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
  38. data/lib/sneakers/middleware/config.rb +23 -0
  39. data/lib/sneakers/publisher.rb +49 -0
  40. data/lib/sneakers/queue.rb +87 -0
  41. data/lib/sneakers/runner.rb +91 -0
  42. data/lib/sneakers/spawner.rb +30 -0
  43. data/lib/sneakers/support/production_formatter.rb +11 -0
  44. data/lib/sneakers/support/utils.rb +18 -0
  45. data/lib/sneakers/tasks.rb +66 -0
  46. data/lib/sneakers/version.rb +3 -0
  47. data/lib/sneakers/worker.rb +162 -0
  48. data/lib/sneakers/workergroup.rb +60 -0
  49. data/lib/sneakers.rb +125 -0
  50. data/log/.gitkeep +0 -0
  51. data/scripts/local_integration +2 -0
  52. data/scripts/local_worker +3 -0
  53. data/spec/fixtures/integration_worker.rb +18 -0
  54. data/spec/fixtures/require_worker.rb +23 -0
  55. data/spec/gzip_helper.rb +15 -0
  56. data/spec/sneakers/cli_spec.rb +75 -0
  57. data/spec/sneakers/concerns/logging_spec.rb +39 -0
  58. data/spec/sneakers/concerns/metrics_spec.rb +38 -0
  59. data/spec/sneakers/configuration_spec.rb +97 -0
  60. data/spec/sneakers/content_encoding_spec.rb +81 -0
  61. data/spec/sneakers/content_type_spec.rb +81 -0
  62. data/spec/sneakers/integration_spec.rb +158 -0
  63. data/spec/sneakers/publisher_spec.rb +179 -0
  64. data/spec/sneakers/queue_spec.rb +169 -0
  65. data/spec/sneakers/runner_spec.rb +70 -0
  66. data/spec/sneakers/sneakers_spec.rb +77 -0
  67. data/spec/sneakers/support/utils_spec.rb +44 -0
  68. data/spec/sneakers/tasks/sneakers_run_spec.rb +115 -0
  69. data/spec/sneakers/worker_handlers_spec.rb +469 -0
  70. data/spec/sneakers/worker_spec.rb +712 -0
  71. data/spec/sneakers/workergroup_spec.rb +83 -0
  72. data/spec/spec_helper.rb +21 -0
  73. metadata +352 -0
@@ -0,0 +1,712 @@
1
+ require 'spec_helper'
2
+ require 'gzip_helper'
3
+ require 'sneakers'
4
+ require 'serverengine'
5
+
6
+ class DummyWorker
7
+ include Sneakers::Worker
8
+ from_queue 'downloads',
9
+ :exchange_options => {
10
+ :type => :topic,
11
+ :durable => false,
12
+ :auto_delete => true,
13
+ :arguments => { 'x-arg' => 'value' }
14
+ },
15
+ :queue_options => {
16
+ :durable => false,
17
+ :auto_delete => true,
18
+ :exclusive => true,
19
+ :arguments => { 'x-arg' => 'value' }
20
+ },
21
+ :ack => false,
22
+ :threads => 50,
23
+ :prefetch => 40,
24
+ :exchange => 'dummy',
25
+ :heartbeat => 5
26
+
27
+ def work(msg)
28
+ end
29
+ end
30
+
31
+ class DefaultsWorker
32
+ include Sneakers::Worker
33
+ from_queue 'defaults'
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
+ class JSONPublishingWorker
69
+ include Sneakers::Worker
70
+ from_queue 'defaults',
71
+ :ack => false,
72
+ :exchange => 'foochange'
73
+
74
+ def work(msg)
75
+ publish msg, :to_queue => 'target', :content_type => 'application/json'
76
+ end
77
+ end
78
+
79
+ class GzipPublishingWorker
80
+ include Sneakers::Worker
81
+ from_queue 'defaults',
82
+ :ack => false,
83
+ :exchange => 'foochange'
84
+
85
+ def work(msg)
86
+ publish msg, :to_queue => 'target', :content_encoding => 'gzip'
87
+ end
88
+ end
89
+
90
+ class LoggingWorker
91
+ include Sneakers::Worker
92
+ from_queue 'defaults',
93
+ :ack => false
94
+
95
+ def work(msg)
96
+ logger.info "hello"
97
+ end
98
+ end
99
+
100
+ class JSONWorker
101
+ include Sneakers::Worker
102
+ from_queue 'defaults',
103
+ :ack => false,
104
+ :content_type => 'application/json'
105
+
106
+ def work(msg)
107
+ end
108
+ end
109
+
110
+ class GzipWorker
111
+ include Sneakers::Worker
112
+ from_queue 'defaults',
113
+ :ack => false,
114
+ :content_encoding => 'gzip'
115
+
116
+ def work(msg)
117
+ end
118
+ end
119
+
120
+ class MetricsWorker
121
+ include Sneakers::Worker
122
+ from_queue 'defaults',
123
+ :ack => true
124
+
125
+ def work(msg)
126
+ metrics.increment "foobar"
127
+ msg
128
+ end
129
+ end
130
+
131
+ class WithParamsWorker
132
+ include Sneakers::Worker
133
+ from_queue 'defaults',
134
+ :ack => true
135
+
136
+ def work_with_params(msg, delivery_info, metadata)
137
+ msg
138
+ end
139
+ end
140
+
141
+ class WithDeprecatedExchangeOptionsWorker
142
+ include Sneakers::Worker
143
+ from_queue 'defaults',
144
+ :durable => false,
145
+ :exchange_type => :topic,
146
+ :exchange_arguments => { 'x-arg' => 'value' },
147
+ :arguments => { 'x-arg2' => 'value2' }
148
+
149
+ def work(msg)
150
+ end
151
+ end
152
+
153
+ TestPool ||= Concurrent::ImmediateExecutor
154
+
155
+ describe Sneakers::Worker do
156
+ before do
157
+ @queue = Object.new
158
+ @exchange = Object.new
159
+ stub(@queue).name { 'test-queue' }
160
+ stub(@queue).opts { {} }
161
+ stub(@queue).exchange { @exchange }
162
+
163
+ Sneakers.clear!
164
+ Sneakers.configure(daemonize: true, log: 'sneakers.log')
165
+ Sneakers::Worker.configure_metrics
166
+ end
167
+
168
+ describe ".enqueue" do
169
+ it "publishes a message to the class queue" do
170
+ message = 'test message'
171
+
172
+ mock(Sneakers::Publisher).new(DummyWorker.queue_opts) do
173
+ mock(Object.new).publish(message, {
174
+ :routing_key => 'test.routing.key',
175
+ :to_queue => 'downloads',
176
+ :content_type => nil,
177
+ :content_encoding => nil,
178
+ })
179
+ end
180
+
181
+ DummyWorker.enqueue(message, :routing_key => 'test.routing.key')
182
+ end
183
+ end
184
+
185
+ describe "#initialize" do
186
+ describe "builds an internal queue" do
187
+ it "should build a queue with correct configuration given defaults" do
188
+ @defaults_q = DefaultsWorker.new.queue
189
+ _(@defaults_q.name).must_equal('defaults')
190
+ _(@defaults_q.opts.to_hash).must_equal(
191
+ :error_reporters => [Sneakers.error_reporters.last],
192
+ :runner_config_file => nil,
193
+ :metrics => nil,
194
+ :daemonize => true,
195
+ :start_worker_delay => 0.2,
196
+ :workers => 4,
197
+ :log => "sneakers.log",
198
+ :pid_path => "sneakers.pid",
199
+ :prefetch => 10,
200
+ :threads => 10,
201
+ :share_threads => false,
202
+ :ack => true,
203
+ :amqp => "amqp://guest:guest@localhost:5672",
204
+ :vhost => "/",
205
+ :exchange => "sneakers",
206
+ :exchange_options => {
207
+ :type => :direct,
208
+ :durable => true,
209
+ :auto_delete => false,
210
+ :arguments => {}
211
+ },
212
+ :queue_options => {
213
+ :durable => true,
214
+ :auto_delete => false,
215
+ :exclusive => false,
216
+ :arguments => {}
217
+ },
218
+ :hooks => {},
219
+ :handler => Sneakers::Handlers::Oneshot,
220
+ :heartbeat => 30,
221
+ :amqp_heartbeat => 30
222
+ )
223
+ end
224
+
225
+ it "should build a queue with given configuration" do
226
+ @dummy_q = DummyWorker.new.queue
227
+ _(@dummy_q.name).must_equal('downloads')
228
+ _(@dummy_q.opts.to_hash).must_equal(
229
+ :error_reporters => [Sneakers.error_reporters.last],
230
+ :runner_config_file => nil,
231
+ :metrics => nil,
232
+ :daemonize => true,
233
+ :start_worker_delay => 0.2,
234
+ :workers => 4,
235
+ :log => "sneakers.log",
236
+ :pid_path => "sneakers.pid",
237
+ :prefetch => 40,
238
+ :threads => 50,
239
+ :share_threads => false,
240
+ :ack => false,
241
+ :amqp => "amqp://guest:guest@localhost:5672",
242
+ :vhost => "/",
243
+ :exchange => "dummy",
244
+ :exchange_options => {
245
+ :type => :topic,
246
+ :durable => false,
247
+ :auto_delete => true,
248
+ :arguments => { 'x-arg' => 'value' }
249
+ },
250
+ :queue_options => {
251
+ :durable => false,
252
+ :auto_delete => true,
253
+ :exclusive => true,
254
+ :arguments => { 'x-arg' => 'value' }
255
+ },
256
+ :hooks => {},
257
+ :handler => Sneakers::Handlers::Oneshot,
258
+ :heartbeat => 5,
259
+ :amqp_heartbeat => 30
260
+ )
261
+ end
262
+
263
+ it "should build a queue with correct configuration given deprecated exchange options" do
264
+ @deprecated_exchange_opts_q = WithDeprecatedExchangeOptionsWorker.new.queue
265
+ _(@deprecated_exchange_opts_q.name).must_equal('defaults')
266
+ _(@deprecated_exchange_opts_q.opts.to_hash).must_equal(
267
+ :error_reporters => [Sneakers.error_reporters.last],
268
+ :runner_config_file => nil,
269
+ :metrics => nil,
270
+ :daemonize => true,
271
+ :start_worker_delay => 0.2,
272
+ :workers => 4,
273
+ :log => "sneakers.log",
274
+ :pid_path => "sneakers.pid",
275
+ :prefetch => 10,
276
+ :threads => 10,
277
+ :share_threads => false,
278
+ :ack => true,
279
+ :amqp => "amqp://guest:guest@localhost:5672",
280
+ :vhost => "/",
281
+ :exchange => "sneakers",
282
+ :exchange_options => {
283
+ :type => :topic,
284
+ :durable => false,
285
+ :auto_delete => false,
286
+ :arguments => { 'x-arg' => 'value' }
287
+ },
288
+ :queue_options => {
289
+ :durable => false,
290
+ :auto_delete => false,
291
+ :exclusive => false,
292
+ :arguments => { 'x-arg2' => 'value2' }
293
+ },
294
+ :hooks => {},
295
+ :handler => Sneakers::Handlers::Oneshot,
296
+ :heartbeat => 30,
297
+ :amqp_heartbeat => 30
298
+ )
299
+ end
300
+ end
301
+
302
+ describe "initializes worker" do
303
+ it "should generate a worker id" do
304
+ _(DummyWorker.new.id).must_match(/^worker-/)
305
+ end
306
+ end
307
+
308
+ describe 'when connection provided' do
309
+ before do
310
+ @connection = Bunny.new(host: 'any-host.local')
311
+ Sneakers.configure(
312
+ exchange: 'some-exch',
313
+ exchange_options: { type: :direct },
314
+ connection: @connection,
315
+ )
316
+ end
317
+
318
+ it "should build a queue with given connection" do
319
+ @dummy_q = DummyWorker.new.queue
320
+ _(@dummy_q.opts[:connection]).must_equal(@connection)
321
+ end
322
+ end
323
+ end
324
+
325
+
326
+ describe "#run" do
327
+ it "should subscribe on internal queue" do
328
+ q = Object.new
329
+ w = DummyWorker.new(q)
330
+ mock(q).subscribe(w).once #XXX once?
331
+ stub(q).name{ "test" }
332
+ stub(q).opts { nil }
333
+ w.run
334
+ end
335
+ end
336
+
337
+ describe "#stop" do
338
+ it "should unsubscribe from internal queue" do
339
+ q = Object.new
340
+ mock(q).unsubscribe.once #XXX once?
341
+ stub(q).name { 'test-queue' }
342
+ stub(q).opts {nil}
343
+ w = DummyWorker.new(q)
344
+ w.stop
345
+ end
346
+ end
347
+
348
+
349
+ describe "#do_work" do
350
+ it "should perform worker's work" do
351
+ w = DummyWorker.new(@queue, TestPool.new)
352
+ mock(w).work("msg").once
353
+ w.do_work(nil, nil, "msg", nil)
354
+ end
355
+
356
+ describe 'content type based deserialization' do
357
+ before do
358
+ Sneakers::ContentType.register(
359
+ content_type: 'application/json',
360
+ serializer: ->(_) {},
361
+ deserializer: ->(payload) { JSON.parse(payload) },
362
+ )
363
+ end
364
+
365
+ after do
366
+ Sneakers::ContentType.reset!
367
+ end
368
+
369
+ it 'should use the registered deserializer if the content type is in the metadata' do
370
+ w = DummyWorker.new(@queue, TestPool.new)
371
+ mock(w).work({'foo' => 'bar'}).once
372
+ w.do_work(nil, { content_type: 'application/json' }, '{"foo":"bar"}', nil)
373
+ end
374
+
375
+ it 'should use the registered deserializer if the content type is in the queue options' do
376
+ w = JSONWorker.new(@queue, TestPool.new)
377
+ mock(w).work({'foo' => 'bar'}).once
378
+ w.do_work(nil, {}, '{"foo":"bar"}', nil)
379
+ end
380
+
381
+ it 'should use the deserializer from the queue options even if the metadata has a different content type' do
382
+ w = JSONWorker.new(@queue, TestPool.new)
383
+ mock(w).work({'foo' => 'bar'}).once
384
+ w.do_work(nil, { content_type: 'not/real' }, '{"foo":"bar"}', nil)
385
+ end
386
+ end
387
+
388
+ describe 'content encoding based decoding' do
389
+ before do
390
+ Sneakers::ContentEncoding.register(
391
+ content_encoding: 'gzip',
392
+ encoder: ->(_) {},
393
+ decoder: ->(payload) { gzip_decompress(payload) },
394
+ )
395
+ end
396
+
397
+ after do
398
+ Sneakers::ContentEncoding.reset!
399
+ end
400
+
401
+ it 'should use the registered decoder if the content encoding is in the metadata' do
402
+ w = DummyWorker.new(@queue, TestPool.new)
403
+ mock(w).work('foobar').once
404
+ w.do_work(nil, { content_encoding: 'gzip' }, gzip_compress('foobar'), nil)
405
+ end
406
+
407
+ it 'should use the registered decoder if the content encoding is in the queue options' do
408
+ w = GzipWorker.new(@queue, TestPool.new)
409
+ mock(w).work('foobar').once
410
+ w.do_work(nil, {}, gzip_compress('foobar'), nil)
411
+ end
412
+
413
+ it 'should use the decoder from the queue options even if the metadata has a different content encoding' do
414
+ w = GzipWorker.new(@queue, TestPool.new)
415
+ mock(w).work('foobar').once
416
+ w.do_work(nil, { content_encoding: 'not/real' }, gzip_compress('foobar'), nil)
417
+ end
418
+ end
419
+
420
+ it "should catch runtime exceptions from a bad work" do
421
+ w = AcksWorker.new(@queue, TestPool.new)
422
+ mock(w).work("msg").once{ raise "foo" }
423
+ handler = Object.new
424
+ header = Object.new
425
+ mock(handler).error(header, nil, "msg", anything)
426
+ mock(w.logger).error(/\[Exception error="foo" error_class=RuntimeError worker_class=AcksWorker backtrace=.*/)
427
+ w.do_work(header, nil, "msg", handler)
428
+ end
429
+
430
+ it "should catch script exceptions from a bad work" do
431
+ w = AcksWorker.new(@queue, TestPool.new)
432
+ mock(w).work("msg").once{ raise ScriptError }
433
+ handler = Object.new
434
+ header = Object.new
435
+ mock(handler).error(header, nil, "msg", anything)
436
+ mock(w.logger).error(/\[Exception error="ScriptError" error_class=ScriptError worker_class=AcksWorker backtrace=.*/)
437
+ w.do_work(header, nil, "msg", handler)
438
+ end
439
+
440
+ it "should log exceptions from workers" do
441
+ handler = Object.new
442
+ header = Object.new
443
+ w = AcksWorker.new(@queue, TestPool.new)
444
+ mock(w).work("msg").once{ raise "foo" }
445
+ mock(w.logger).error(/error="foo" error_class=RuntimeError worker_class=AcksWorker backtrace=/)
446
+ mock(handler).error(header, nil, "msg", anything)
447
+ w.do_work(header, nil, "msg", handler)
448
+ end
449
+
450
+ describe 'middleware' do
451
+ let(:middleware) do
452
+ Class.new do
453
+ def initialize(app, *args)
454
+ @app = app
455
+ end
456
+
457
+ def call(deserialized_msg, delivery_info, metadata, handler)
458
+ @app.call(deserialized_msg, delivery_info, metadata, handler)
459
+ end
460
+ end
461
+ end
462
+
463
+ let(:worker) do
464
+ Class.new do
465
+ include Sneakers::Worker
466
+ from_queue 'defaults', ack: false
467
+
468
+ def work_with_params(msg, delivery_info, metadata)
469
+ msg
470
+ end
471
+ end
472
+ end
473
+
474
+ before do
475
+ Sneakers.middleware.use(middleware, 'args')
476
+
477
+ @delivery_info = Object.new
478
+ @metadata = Object.new
479
+ stub(@metadata).[](:content_type) { 'some/fake' }
480
+ stub(@metadata).[](:content_encoding) { 'some/fake' }
481
+ @message = Object.new
482
+ @handler = Object.new
483
+ end
484
+
485
+ after do
486
+ Sneakers.middleware.delete(middleware)
487
+ end
488
+
489
+ it 'should process job and call #work_with_params/#work' do
490
+ w = worker.new(@queue, TestPool.new)
491
+ mock(w).work_with_params(@message, @delivery_info, @metadata).once
492
+
493
+ w.do_work(@delivery_info, @metadata, @message, @handler)
494
+ end
495
+
496
+ it "should call registered middleware" do
497
+ mock.proxy(middleware).new(instance_of(Proc), 'args').once do |res|
498
+ mock.proxy(res).call(@message, @delivery_info, @metadata, @handler).once
499
+ end
500
+
501
+ w = worker.new(@queue, TestPool.new)
502
+ w.do_work(@delivery_info, @metadata, @message, @handler)
503
+ end
504
+ end
505
+
506
+ describe "with ack" do
507
+ before do
508
+ @delivery_info = Object.new
509
+ stub(@delivery_info).delivery_tag{ "tag" }
510
+
511
+ @worker = AcksWorker.new(@queue, TestPool.new)
512
+ end
513
+
514
+ it "should work and handle acks" do
515
+ handler = Object.new
516
+ mock(handler).acknowledge(@delivery_info, nil, :ack)
517
+
518
+ @worker.do_work(@delivery_info, nil, :ack, handler)
519
+ end
520
+
521
+ it "should work and handle rejects" do
522
+ handler = Object.new
523
+ mock(handler).reject(@delivery_info, nil, :reject)
524
+
525
+ @worker.do_work(@delivery_info, nil, :reject, handler)
526
+ end
527
+
528
+ it "should work and handle requeues" do
529
+ handler = Object.new
530
+ mock(handler).reject(@delivery_info, nil, :requeue, true)
531
+
532
+ @worker.do_work(@delivery_info, nil, :requeue, handler)
533
+ end
534
+
535
+ it "should work and handle user code errors" do
536
+ handler = Object.new
537
+ mock(handler).error(@delivery_info, nil, :error, anything)
538
+
539
+ @worker.do_work(@delivery_info, nil, :error, handler)
540
+ end
541
+ end
542
+
543
+ describe "without ack" do
544
+ it "should work and not care about acking if not ack" do
545
+ handler = Object.new
546
+ mock(handler).reject(anything).never
547
+ mock(handler).acknowledge(anything).never
548
+
549
+ w = DummyWorker.new(@queue, TestPool.new)
550
+ w.do_work(nil, nil, 'msg', handler)
551
+ end
552
+ end
553
+ end
554
+
555
+
556
+ describe 'publish' do
557
+ it 'should be able to publish a message from working context' do
558
+ w = PublishingWorker.new(@queue, TestPool.new)
559
+ mock(@exchange).publish('msg', :routing_key => 'target').once
560
+ w.do_work(nil, nil, 'msg', nil)
561
+ end
562
+
563
+ it 'should be able to publish arbitrary metadata' do
564
+ w = PublishingWorker.new(@queue, TestPool.new)
565
+ mock(@exchange).publish('msg', :routing_key => 'target', :expiration => 1).once
566
+ w.publish 'msg', :to_queue => 'target', :expiration => 1
567
+ end
568
+
569
+ describe 'content_type based serialization' do
570
+ before do
571
+ Sneakers::ContentType.register(
572
+ content_type: 'application/json',
573
+ serializer: ->(payload) { JSON.dump(payload) },
574
+ deserializer: ->(_) {},
575
+ )
576
+ end
577
+
578
+ after do
579
+ Sneakers::ContentType.reset!
580
+ end
581
+
582
+ it 'should be able to publish a message from working context' do
583
+ w = JSONPublishingWorker.new(@queue, TestPool.new)
584
+ mock(@exchange).publish('{"foo":"bar"}', :routing_key => 'target', :content_type => 'application/json').once
585
+ w.do_work(nil, {}, {'foo' => 'bar'}, nil)
586
+ end
587
+ end
588
+
589
+ describe 'content_encoding based encoding' do
590
+ before do
591
+ Sneakers::ContentEncoding.register(
592
+ content_encoding: 'gzip',
593
+ encoder: ->(payload) { gzip_compress(payload) },
594
+ decoder: ->(_) {},
595
+ )
596
+ end
597
+
598
+ after do
599
+ Sneakers::ContentEncoding.reset!
600
+ end
601
+
602
+ it 'should be able to publish a message from working context' do
603
+ w = GzipPublishingWorker.new(@queue, TestPool.new)
604
+ mock(@exchange).publish(gzip_compress('foobar'), :routing_key => 'target', :content_encoding => 'gzip').once
605
+ w.do_work(nil, {}, 'foobar', nil)
606
+ end
607
+ end
608
+ end
609
+
610
+
611
+ describe 'Logging' do
612
+ it 'should be able to use the logging facilities' do
613
+ log = Logger.new('/dev/null')
614
+ mock(log).debug(anything).once
615
+ mock(log).info("hello").once
616
+ Sneakers::Worker.configure_logger(log)
617
+
618
+ w = LoggingWorker.new(@queue, TestPool.new)
619
+ w.do_work(nil,nil,'msg',nil)
620
+ end
621
+
622
+ it 'has a helper to constuct log prefix values' do
623
+ w = DummyWorker.new(@queue, TestPool.new)
624
+ w.instance_variable_set(:@id, 'worker-id')
625
+ m = w.log_msg('foo')
626
+ _(w.log_msg('foo')).must_match(/\[worker-id\]\[#<Thread:.*>\]\[test-queue\]\[\{\}\] foo/)
627
+ end
628
+
629
+ describe '#worker_error' do
630
+ it 'only logs backtraces if present' do
631
+ w = DummyWorker.new(@queue, TestPool.new)
632
+ mock(w.logger).warn('cuz')
633
+ mock(w.logger).error(/\[Exception error="boom!" error_class=RuntimeError worker_class=DummyWorker\]/)
634
+ w.worker_error(RuntimeError.new('boom!'), 'cuz')
635
+ end
636
+ end
637
+ end
638
+
639
+
640
+ describe 'Metrics' do
641
+ before do
642
+ @handler = Object.new
643
+ @header = Object.new
644
+
645
+ # We don't care how these are called, we're focusing on metrics here.
646
+ stub(@handler).acknowledge
647
+ stub(@handler).reject
648
+ stub(@handler).error
649
+ stub(@handler).noop
650
+
651
+ @delivery_info = Object.new
652
+ stub(@delivery_info).delivery_tag { "tag" }
653
+
654
+ @w = MetricsWorker.new(@queue, TestPool.new)
655
+ mock(@w.metrics).increment("work.MetricsWorker.started").once
656
+ mock(@w.metrics).increment("work.MetricsWorker.ended").once
657
+ mock(@w.metrics).timing("work.MetricsWorker.time").yields.once
658
+ end
659
+
660
+ it 'should be able to meter acks' do
661
+ mock(@w.metrics).increment("foobar").once
662
+ mock(@w.metrics).increment("work.MetricsWorker.handled.ack").once
663
+ @w.do_work(@delivery_info, nil, :ack, @handler)
664
+ end
665
+
666
+ it 'should be able to meter rejects' do
667
+ mock(@w.metrics).increment("foobar").once
668
+ mock(@w.metrics).increment("work.MetricsWorker.handled.reject").once
669
+ @w.do_work(@header, nil, :reject, @handler)
670
+ end
671
+
672
+ it 'should be able to meter requeue' do
673
+ mock(@w.metrics).increment("foobar").once
674
+ mock(@w.metrics).increment("work.MetricsWorker.handled.requeue").once
675
+ @w.do_work(@header, nil, :requeue, @handler)
676
+ end
677
+
678
+ it 'should be able to meter errors' do
679
+ mock(@w.metrics).increment("work.MetricsWorker.handled.error").once
680
+ mock(@w).work('msg'){ raise :error }
681
+ @w.do_work(@delivery_info, nil, 'msg', @handler)
682
+ end
683
+
684
+ it 'defaults to noop when no response is specified' do
685
+ mock(@w.metrics).increment("foobar").once
686
+ mock(@w.metrics).increment("work.MetricsWorker.handled.noop").once
687
+ @w.do_work(@header, nil, nil, @handler)
688
+ end
689
+ end
690
+
691
+
692
+
693
+ describe 'With Params' do
694
+ before do
695
+ @props = { :foo => 1 }
696
+ @handler = Object.new
697
+ @header = Object.new
698
+
699
+ @delivery_info = Object.new
700
+
701
+ stub(@handler).noop(@delivery_info, {:foo => 1}, :ack)
702
+
703
+ @w = WithParamsWorker.new(@queue, TestPool.new)
704
+ mock(@w.metrics).timing("work.WithParamsWorker.time").yields.once
705
+ end
706
+
707
+ it 'should call work_with_params and not work' do
708
+ mock(@w).work_with_params(:ack, @delivery_info, {:foo => 1}).once
709
+ @w.do_work(@delivery_info, {:foo => 1 }, :ack, @handler)
710
+ end
711
+ end
712
+ end