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,469 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'sneakers/handlers/oneshot'
4
+ require 'sneakers/handlers/maxretry'
5
+ require 'json'
6
+
7
+
8
+ # Specific tests of the Handler implementations you can use to deal with job
9
+ # results. These tests only make sense with a worker that requires acking.
10
+
11
+ class HandlerTestWorker
12
+ include Sneakers::Worker
13
+ from_queue 'defaults',
14
+ :ack => true
15
+
16
+ def work(msg)
17
+ if msg.is_a?(StandardError)
18
+ raise msg
19
+ elsif msg.is_a?(String)
20
+ hash = maybe_json(msg)
21
+ if hash.is_a?(Hash)
22
+ hash['response'].to_sym
23
+ else
24
+ hash
25
+ end
26
+ else
27
+ msg
28
+ end
29
+ end
30
+
31
+ def maybe_json(string)
32
+ JSON.parse(string)
33
+ rescue
34
+ string
35
+ end
36
+ end
37
+
38
+ TestPool ||= Concurrent::ImmediateExecutor
39
+
40
+ describe 'Handlers' do
41
+ let(:channel) { Object.new }
42
+ let(:queue) { Object.new }
43
+ let(:worker) { HandlerTestWorker.new(@queue, TestPool.new) }
44
+
45
+ before(:each) do
46
+ Sneakers.configure(:daemonize => true, :log => 'sneakers.log')
47
+ Sneakers::Worker.configure_logger(Logger.new('/dev/null'))
48
+ Sneakers::Worker.configure_metrics
49
+ end
50
+
51
+ describe 'Oneshot' do
52
+ before(:each) do
53
+ @opts = Object.new
54
+ @handler = Sneakers::Handlers::Oneshot.new(channel, queue, @opts)
55
+
56
+ @header = Object.new
57
+ stub(@header).delivery_tag { 37 }
58
+ end
59
+
60
+ describe '#do_work' do
61
+ it 'should work and handle acks' do
62
+ mock(channel).acknowledge(37, false)
63
+
64
+ worker.do_work(@header, nil, :ack, @handler)
65
+ end
66
+
67
+ it 'should work and handle rejects' do
68
+ mock(channel).reject(37, false)
69
+
70
+ worker.do_work(@header, nil, :reject, @handler)
71
+ end
72
+
73
+ it 'should work and handle requeues' do
74
+ mock(channel).reject(37, true)
75
+
76
+ worker.do_work(@header, nil, :requeue, @handler)
77
+ end
78
+
79
+ it 'should work and handle user code error' do
80
+ mock(channel).reject(37, false)
81
+
82
+ worker.do_work(@header, nil, StandardError.new('boom!'), @handler)
83
+ end
84
+
85
+ it 'should work and handle noops' do
86
+ worker.do_work(@header, nil, :wait, @handler)
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ describe 'Maxretry' do
93
+ let(:max_retries) { nil }
94
+ let(:props_with_x_death_count) {
95
+ {
96
+ :headers => {
97
+ "x-death" => [
98
+ {
99
+ "count" => 3,
100
+ "reason" => "expired",
101
+ "queue" => "downloads-retry",
102
+ "time" => Time.now,
103
+ "exchange" => "RawMail-retry",
104
+ "routing-keys" => ["RawMail"]
105
+ },
106
+ {
107
+ "count" => 3,
108
+ "reason" => "rejected",
109
+ "queue" => "downloads",
110
+ "time" => Time.now,
111
+ "exchange" => "",
112
+ "routing-keys" => ["RawMail"]
113
+ }
114
+ ]
115
+ },
116
+ :delivery_mode => 1
117
+ }
118
+ }
119
+
120
+ before(:each) do
121
+ @opts = {
122
+ :exchange => 'sneakers',
123
+ :queue_options => {
124
+ :durable => 'true',
125
+ }
126
+ }.tap do |opts|
127
+ opts[:retry_max_times] = max_retries unless max_retries.nil?
128
+ end
129
+
130
+ mock(queue).name { 'downloads' }
131
+
132
+ @retry_exchange = Object.new
133
+ @error_exchange = Object.new
134
+ @requeue_exchange = Object.new
135
+
136
+ @retry_queue = Object.new
137
+ @error_queue = Object.new
138
+
139
+ mock(channel).exchange('downloads-retry',
140
+ :type => 'topic',
141
+ :durable => 'true').once { @retry_exchange }
142
+ mock(channel).exchange('downloads-error',
143
+ :type => 'topic',
144
+ :durable => 'true').once { @error_exchange }
145
+ mock(channel).exchange('downloads-retry-requeue',
146
+ :type => 'topic',
147
+ :durable => 'true').once { @requeue_exchange }
148
+
149
+ mock(channel).queue('downloads-retry',
150
+ :durable => 'true',
151
+ :arguments => {
152
+ :'x-dead-letter-exchange' => 'downloads-retry-requeue',
153
+ :'x-message-ttl' => 60000
154
+ }
155
+ ).once { @retry_queue }
156
+ mock(@retry_queue).bind(@retry_exchange, :routing_key => '#')
157
+
158
+ mock(channel).queue('downloads-error',
159
+ :durable => 'true').once { @error_queue }
160
+ mock(@error_queue).bind(@error_exchange, :routing_key => '#')
161
+
162
+ @header = Object.new
163
+ stub(@header).delivery_tag { 37 }
164
+
165
+ @props = {}
166
+ @props_with_x_death = {
167
+ :headers => {
168
+ "x-death" => [
169
+ {
170
+ "reason" => "expired",
171
+ "queue" => "downloads-retry",
172
+ "time" => Time.now,
173
+ "exchange" => "RawMail-retry",
174
+ "routing-keys" => ["RawMail"]
175
+ },
176
+ {
177
+ "reason" => "rejected",
178
+ "queue" => "downloads",
179
+ "time" => Time.now,
180
+ "exchange" => "",
181
+ "routing-keys" => ["RawMail"]
182
+ }
183
+ ]
184
+ },
185
+ :delivery_mode => 1}
186
+ end
187
+
188
+ # it 'allows overriding the retry exchange name'
189
+ # it 'allows overriding the error exchange name'
190
+
191
+ describe '#do_work' do
192
+ before do
193
+ @now = Time.now
194
+
195
+ mock(queue).bind(@requeue_exchange, :routing_key => '#')
196
+
197
+ @handler = Sneakers::Handlers::Maxretry.new(channel, queue, @opts)
198
+ end
199
+
200
+ # Used to stub out the publish method args. Sadly RR doesn't support
201
+ # this, only proxying existing methods.
202
+ module MockPublish
203
+ attr_reader :data, :opts, :called
204
+
205
+ def publish(data, opts)
206
+ @data = data
207
+ @opts = opts
208
+ @called = true
209
+ end
210
+ end
211
+
212
+ it 'should work and handle acks' do
213
+ mock(channel).acknowledge(37, false)
214
+
215
+ worker.do_work(@header, @props, :ack, @handler)
216
+ end
217
+
218
+ describe 'rejects' do
219
+ describe 'more retries ahead' do
220
+ it 'should work and handle rejects' do
221
+ mock(channel).reject(37, false)
222
+
223
+ worker.do_work(@header, @props_with_x_death, :reject, @handler)
224
+ end
225
+ end
226
+
227
+ describe 'no more retries' do
228
+ let(:max_retries) { 1 }
229
+
230
+ it 'sends the rejection to the error queue' do
231
+ mock(@header).routing_key { '#' }
232
+ mock(channel).acknowledge(37, false)
233
+
234
+ @error_exchange.extend MockPublish
235
+ worker.do_work(@header, @props_with_x_death, :reject, @handler)
236
+ _(@error_exchange.called).must_equal(true)
237
+ _(@error_exchange.opts[:routing_key]).must_equal('#')
238
+ data = JSON.parse(@error_exchange.opts[:headers][:retry_info]) rescue nil
239
+ _(data).wont_be_nil
240
+ _(data['error']).must_equal('reject')
241
+ _(data['num_attempts']).must_equal(2)
242
+ _(@error_exchange.data).must_equal(:reject)
243
+ _(data['properties'].to_json).must_equal(@props_with_x_death.to_json)
244
+ _(Time.parse(data['failed_at'])).wont_be_nil
245
+ end
246
+
247
+ it 'counts the number of attempts using the count key' do
248
+ mock(@header).routing_key { '#' }
249
+ mock(channel).acknowledge(37, false)
250
+
251
+ @error_exchange.extend MockPublish
252
+ worker.do_work(@header, props_with_x_death_count, :reject, @handler)
253
+ _(@error_exchange.called).must_equal(true)
254
+ _(@error_exchange.opts[:routing_key]).must_equal('#')
255
+ data = JSON.parse(@error_exchange.opts[:headers][:retry_info]) rescue nil
256
+ _(data).wont_be_nil
257
+ _(data['error']).must_equal('reject')
258
+ _(data['num_attempts']).must_equal(4)
259
+ _(@error_exchange.data).must_equal(:reject)
260
+ _(data['properties'].to_json).must_equal(props_with_x_death_count.to_json)
261
+ _(Time.parse(data['failed_at'])).wont_be_nil
262
+ end
263
+
264
+ end
265
+ end
266
+
267
+ describe 'requeues' do
268
+ it 'should work and handle requeues' do
269
+ mock(channel).reject(37, true)
270
+
271
+ worker.do_work(@header, @props_with_x_death, :requeue, @handler)
272
+ end
273
+
274
+ describe 'no more retries left' do
275
+ let(:max_retries) { 1 }
276
+
277
+ it 'continues to reject with requeue' do
278
+ mock(channel).reject(37, true)
279
+
280
+ worker.do_work(@header, @props_with_x_death, :requeue, @handler)
281
+ end
282
+ end
283
+
284
+ end
285
+
286
+ describe 'exceptions' do
287
+ describe 'more retries ahead' do
288
+ it 'should reject the message' do
289
+ mock(channel).reject(37, false)
290
+
291
+ worker.do_work(@header, @props_with_x_death, StandardError.new('boom!'), @handler)
292
+ end
293
+ end
294
+
295
+ describe 'no more retries left' do
296
+ let(:max_retries) { 1 }
297
+
298
+ it 'sends the rejection to the error queue' do
299
+ mock(@header).routing_key { '#' }
300
+ mock(channel).acknowledge(37, false)
301
+ @error_exchange.extend MockPublish
302
+
303
+ worker.do_work(@header, @props_with_x_death, StandardError.new('boom!'), @handler)
304
+ _(@error_exchange.called).must_equal(true)
305
+ _(@error_exchange.opts[:routing_key]).must_equal('#')
306
+ data = JSON.parse(@error_exchange.opts[:headers][:retry_info]) rescue nil
307
+ _(data).wont_be_nil
308
+ _(data['error']).must_equal('boom!')
309
+ _(data['error_class']).must_equal(StandardError.to_s)
310
+ _(data['backtrace']).wont_be_nil
311
+ _(data['num_attempts']).must_equal(2)
312
+ _(@error_exchange.data.to_s).must_equal('boom!')
313
+ _(data['properties'].to_json).must_equal(@props_with_x_death.to_json)
314
+ _(Time.parse(data['failed_at'])).wont_be_nil
315
+ end
316
+ end
317
+ end
318
+
319
+ it 'should work and handle user-land error' do
320
+ mock(channel).reject(37, false)
321
+
322
+ worker.do_work(@header, @props, StandardError.new('boom!'), @handler)
323
+ end
324
+
325
+ it 'should work and handle noops' do
326
+ worker.do_work(@header, @props, :wait, @handler)
327
+ end
328
+ end
329
+
330
+ describe '.configure_queue' do
331
+ before do
332
+ mock(channel).prefetch(10)
333
+ @mkbunny = Object.new
334
+ @mkex = Object.new
335
+ @mkworker = Object.new
336
+
337
+ mock(@mkbunny).start {}
338
+ mock(@mkbunny).create_channel{ channel }
339
+ mock(Bunny).new(
340
+ anything,
341
+ hash_including(:vhost => '/', :heartbeat => 2)
342
+ ){ @mkbunny }
343
+
344
+ mock(channel).exchange("sneakers",
345
+ :type => :direct,
346
+ :durable => 'true',
347
+ :auto_delete => false,
348
+ :arguments => {}).once { @mkex }
349
+ end
350
+
351
+ describe 'use queue name for retry exchange' do
352
+ before do
353
+ Sneakers.clear!
354
+ Sneakers.configure({
355
+ :connection => nil,
356
+ :ack => true,
357
+ :heartbeat => 2,
358
+ :vhost => '/',
359
+ :exchange => "sneakers",
360
+ :exchange_options => {
361
+ :type => :direct,
362
+ durable: 'true'
363
+ },
364
+ :queue_options => {
365
+ :durable => 'true'
366
+ },
367
+ :handler => Sneakers::Handlers::Maxretry
368
+ })
369
+ end
370
+
371
+ describe 'default settings' do
372
+ before do
373
+ mock(queue).bind(@requeue_exchange, :routing_key => '#')
374
+ @worker_opts = Sneakers::CONFIG.merge({})
375
+ stub(@mkworker).opts { @worker_opts }
376
+ end
377
+
378
+ let(:q) { Sneakers::Queue.new("downloads", @worker_opts) }
379
+
380
+ it 'should configure queue with x-dead-letter-exchange' do
381
+ mock(channel).queue("downloads", :durable => 'true', :auto_delete => false, :exclusive => false, :arguments => { :"x-dead-letter-exchange" => "downloads-retry" }).once { queue }
382
+ mock(queue).bind(@mkex, :routing_key => "downloads")
383
+ mock(queue).subscribe(:block => false, :manual_ack => true)
384
+
385
+ q.subscribe(@mkworker)
386
+ end
387
+ end
388
+
389
+ describe 'preserve other worker arguments' do
390
+ before do
391
+ mock(queue).bind(@requeue_exchange, :routing_key => '#')
392
+ @worker_opts = Sneakers::CONFIG.merge({ :arguments => { 'x-arg' => 'value' } })
393
+ stub(@mkworker).opts { @worker_opts }
394
+ end
395
+
396
+ let(:q) { Sneakers::Queue.new("downloads", @worker_opts) }
397
+
398
+ it 'should configure queue with x-dead-letter-exchange and other args' do
399
+ mock(channel).queue("downloads", :durable => 'true', :auto_delete => false, :exclusive => false, :arguments => { :"x-dead-letter-exchange" => "downloads-retry", :"x-arg" => 'value' }).once { queue }
400
+ mock(queue).bind(@mkex, :routing_key => "downloads")
401
+ mock(queue).subscribe(:block => false, :manual_ack => true)
402
+
403
+ q.subscribe(@mkworker)
404
+ end
405
+ end
406
+ end
407
+
408
+ describe 'use globally configured retry exchange name' do
409
+ before do
410
+ Sneakers.clear!
411
+ Sneakers.configure({
412
+ :connection => nil,
413
+ :ack => true,
414
+ :heartbeat => 2,
415
+ :vhost => '/',
416
+ :exchange => "sneakers",
417
+ :exchange_options => {
418
+ :type => :direct,
419
+ durable: 'true'
420
+ },
421
+ :queue_options => {
422
+ :durable => 'true'
423
+ },
424
+ :handler => Sneakers::Handlers::Maxretry,
425
+ :retry_exchange => "downloads-retry",
426
+ :retry_error_exchange => "downloads-error",
427
+ :retry_requeue_exchange => "downloads-retry-requeue"
428
+ })
429
+ end
430
+
431
+ describe 'use global setup for worker' do
432
+ before do
433
+ mock(queue).bind(@requeue_exchange, :routing_key => 'uploads')
434
+ @worker_opts = Sneakers::CONFIG.merge({ :retry_routing_key => "uploads" })
435
+ stub(@mkworker).opts { @worker_opts }
436
+ end
437
+
438
+ let(:q) { Sneakers::Queue.new("uploads", @worker_opts) }
439
+
440
+ it 'should configure queue with x-dead-letter-exchange (not use queue name)' do
441
+ mock(channel).queue("uploads", :durable => 'true', :auto_delete => false, :exclusive => false, :arguments => { :"x-dead-letter-exchange" => "downloads-retry" }).once { queue }
442
+ mock(queue).bind(@mkex, :routing_key => "uploads")
443
+ mock(queue).subscribe(:block => false, :manual_ack => true)
444
+
445
+ q.subscribe(@mkworker)
446
+ end
447
+ end
448
+
449
+ describe 'skip retry and go to error queue' do
450
+ before do
451
+ mock(queue).bind(@requeue_exchange, :routing_key => 'uploads')
452
+ @worker_opts = Sneakers::CONFIG.merge({ :retry_routing_key => "uploads", :arguments => { :"x-dead-letter-exchange" => "downloads-error" } })
453
+ stub(@mkworker).opts { @worker_opts }
454
+ end
455
+
456
+ let(:q) { Sneakers::Queue.new("uploads", @worker_opts) }
457
+
458
+ it 'should configure queue with x-dead-letter-exchange (not use queue name)' do
459
+ mock(channel).queue("uploads", :durable => 'true', :auto_delete => false, :exclusive => false, :arguments => { :"x-dead-letter-exchange" => "downloads-error" }).once { queue }
460
+ mock(queue).bind(@mkex, :routing_key => "uploads")
461
+ mock(queue).subscribe(:block => false, :manual_ack => true)
462
+
463
+ q.subscribe(@mkworker)
464
+ end
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end