kicks 3.0.0.pre

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