backburner 1.8.0 → 1.9.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -4
  3. data/README.md +70 -14
  4. data/lib/active_job/queue_adapters/backburner_adapter.rb +9 -2
  5. data/lib/backburner/cli.rb +1 -1
  6. data/lib/backburner/enqueue_pool.rb +2 -0
  7. data/lib/backburner/performable.rb +28 -3
  8. data/lib/backburner/version.rb +1 -1
  9. data/lib/backburner/worker.rb +38 -4
  10. data/lib/backburner/workers/threading.rb +16 -0
  11. data/lib/backburner/workers/threads_on_fork.rb +1 -0
  12. metadata +8 -82
  13. data/.github/workflows/ci.yml +0 -45
  14. data/.github/workflows/github-release.yml +0 -58
  15. data/.github/workflows/release.yml +0 -221
  16. data/.gitignore +0 -17
  17. data/CONTRIBUTING.md +0 -37
  18. data/Gemfile +0 -7
  19. data/Rakefile +0 -17
  20. data/TODO +0 -4
  21. data/backburner.gemspec +0 -31
  22. data/examples/custom.rb +0 -25
  23. data/examples/demo.rb +0 -60
  24. data/examples/god.rb +0 -46
  25. data/examples/hooked.rb +0 -87
  26. data/examples/retried.rb +0 -31
  27. data/examples/simple.rb +0 -43
  28. data/examples/stress.rb +0 -31
  29. data/test/active_job_adapter_test.rb +0 -24
  30. data/test/async_proxy_test.rb +0 -36
  31. data/test/back_burner_test.rb +0 -101
  32. data/test/connection_test.rb +0 -201
  33. data/test/fixtures/active_job_app.rb +0 -53
  34. data/test/fixtures/active_jobs.rb +0 -11
  35. data/test/fixtures/autoload/test_broken_name_job.rb +0 -4
  36. data/test/fixtures/autoload/test_broken_no_method_job.rb +0 -4
  37. data/test/fixtures/autoload/test_broken_require_job.rb +0 -4
  38. data/test/fixtures/autoload/test_broken_runtime_job.rb +0 -4
  39. data/test/fixtures/autoload/test_broken_syntax_job.rb +0 -4
  40. data/test/fixtures/autoload/test_broken_timeout_job.rb +0 -4
  41. data/test/fixtures/autoload/test_same_name_job.rb +0 -5
  42. data/test/fixtures/hooked.rb +0 -122
  43. data/test/fixtures/test_fork_jobs.rb +0 -77
  44. data/test/fixtures/test_forking_jobs.rb +0 -56
  45. data/test/fixtures/test_jobs.rb +0 -140
  46. data/test/fixtures/test_queue_settings.rb +0 -54
  47. data/test/helpers/templogger.rb +0 -22
  48. data/test/helpers_test.rb +0 -462
  49. data/test/hooks_test.rb +0 -112
  50. data/test/job_test.rb +0 -207
  51. data/test/logger_test.rb +0 -71
  52. data/test/performable_test.rb +0 -138
  53. data/test/queue_test.rb +0 -132
  54. data/test/test_helper.rb +0 -182
  55. data/test/worker_test.rb +0 -502
  56. data/test/workers/forking_worker_test.rb +0 -199
  57. data/test/workers/simple_worker_test.rb +0 -622
  58. data/test/workers/threading_worker_test.rb +0 -125
  59. data/test/workers/threads_on_fork_worker_test.rb +0 -751
@@ -1,751 +0,0 @@
1
- require File.expand_path('../../test_helper', __FILE__)
2
- require File.expand_path('../../fixtures/test_fork_jobs', __FILE__)
3
- require File.expand_path('../../fixtures/test_queue_settings', __FILE__)
4
-
5
- describe "Backburner::Workers::ThreadsOnFork module" do
6
-
7
- before do
8
- Backburner.default_queues.clear
9
- @worker_class = Backburner::Workers::ThreadsOnFork
10
- @worker_class.shutdown = false
11
- @worker_class.is_child = false
12
- @worker_class.threads_number = 1
13
- @worker_class.garbage_after = 1
14
- @ignore_forks = false
15
- end
16
-
17
- after do
18
- Backburner.configure { |config| config.max_job_retries = 0; config.retry_delay = 5; config.logger = nil }
19
- unless @ignore_forks
20
- cpids = @worker_class.instance_variable_get("@child_pids")
21
- if cpids && cpids.length > 0
22
- raise "Why is there forks alive?"
23
- end
24
- end
25
- end
26
-
27
- describe "for process_tube_names method" do
28
- it "should interpreter the job_name:threads_limit:garbage_after:retries format" do
29
- worker = @worker_class.new(["foo:1:2:3"])
30
- assert_equal ["foo"], worker.tube_names
31
- end
32
-
33
- it "should interpreter event if is missing values" do
34
- tubes = %W(foo1:1:2:3 foo2:4:5 foo3:6 foo4 foo5::7:8 foo6:::9 foo7::10)
35
- worker = @worker_class.new(tubes)
36
- assert_equal %W(foo1 foo2 foo3 foo4 foo5 foo6 foo7), worker.tube_names
37
- end
38
-
39
- it "should store interpreted values correctly" do
40
- tubes = %W(foo1:1:2:3 foo2:4:5 foo3:6 foo4 foo5::7:8 foo6:::9 foo7::10)
41
- worker = @worker_class.new(tubes)
42
- assert_equal({
43
- "demo.test.foo1" => { :threads => 1, :garbage => 2, :retries => 3 },
44
- "demo.test.foo2" => { :threads => 4, :garbage => 5, :retries => nil },
45
- "demo.test.foo3" => { :threads => 6, :garbage => nil, :retries => nil },
46
- "demo.test.foo4" => { :threads => nil, :garbage => nil, :retries => nil },
47
- "demo.test.foo5" => { :threads => nil, :garbage => 7, :retries => 8 },
48
- "demo.test.foo6" => { :threads => nil, :garbage => nil, :retries => 9 },
49
- "demo.test.foo7" => { :threads => nil, :garbage => 10, :retries => nil }
50
- }, worker.instance_variable_get("@tubes_data"))
51
- end
52
- end
53
-
54
- describe "for process_tube_settings" do
55
- it "should set the settings specified by queue name in class" do
56
- worker = @worker_class.new
57
- assert_equal(worker.instance_variable_get("@tubes_data")['demo.test.job-settings'], { :threads => 5, :garbage => 10, :retries => 6 })
58
- end
59
-
60
- it 'should override the tube settings if they are specified directly at class level' do
61
- worker = @worker_class.new
62
- assert_equal(worker.instance_variable_get("@tubes_data")['demo.test.job-settings-override'], { :threads => 10, :garbage => 1000, :retries => 2 })
63
- end
64
-
65
- it "should take a shared tube's settings from the class that owns the tube, not from subclasses" do
66
- tubes_data = @worker_class.new.instance_variable_get("@tubes_data")
67
- assert_equal({ :threads => 5, :garbage => 100, :retries => 3 }, tubes_data['demo.test.tube-limits'])
68
- assert_equal({ :threads => 3, :garbage => nil, :retries => nil }, tubes_data['demo.test.tube-limits-testtubelimitslambdajob'])
69
- assert_equal({ :threads => 4, :garbage => nil, :retries => nil }, tubes_data['demo.test.tube-limits-testtubelimitslambdachildjob'])
70
- end
71
-
72
- it "should fill in the settings a shared tube's class leaves unset from its subclasses" do
73
- tubes_data = @worker_class.new.instance_variable_get("@tubes_data")
74
- assert_equal({ :threads => 3, :garbage => 50, :retries => 2 }, tubes_data['demo.test.tube-few-limits'])
75
- end
76
- end
77
-
78
- describe "for prepare method" do
79
- before do
80
- Backburner.configure { |config| config.logger = false }
81
- end
82
-
83
- it "should make tube names array always unique to avoid duplication" do
84
- worker = @worker_class.new(["foo", "demo.test.foo"])
85
- capture_stdout { worker.prepare }
86
- assert_equal ["demo.test.foo"], worker.tube_names
87
- end
88
-
89
- it "should watch specified tubes" do
90
- worker = @worker_class.new(["foo", "bar"])
91
- out = capture_stdout { worker.prepare }
92
- assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
93
- assert_match(/demo\.test\.foo/, out)
94
- end # multiple
95
-
96
- it "should watch single tube" do
97
- worker = @worker_class.new("foo")
98
- out = capture_stdout { worker.prepare }
99
- assert_equal ["demo.test.foo"], worker.tube_names
100
- assert_match(/demo\.test\.foo/, out)
101
- end # single
102
-
103
- it "should respect default_queues settings" do
104
- Backburner.default_queues.concat(["foo", "bar"])
105
- worker = @worker_class.new
106
- out = capture_stdout { worker.prepare }
107
- assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
108
- assert_match(/demo\.test\.foo/, out)
109
- end
110
-
111
- it "should assign based on all tubes" do
112
- @worker_class.any_instance.expects(:all_existing_queues).once.returns("bar")
113
- worker = @worker_class.new
114
- out = capture_stdout { worker.prepare }
115
- assert_equal ["demo.test.bar"], worker.tube_names
116
- assert_match(/demo\.test\.bar/, out)
117
- end # all assign
118
-
119
- it "should properly retrieve all tubes" do
120
- worker = @worker_class.new
121
- out = capture_stdout { worker.prepare }
122
- assert_contains worker.tube_names, "demo.test.test-job-fork"
123
- assert_match(/demo\.test\.test-job-fork/, out)
124
- end # all read
125
- end # prepare
126
-
127
- describe "forking and threading" do
128
-
129
- it "start should call fork_and_watch for each tube" do
130
- worker = @worker_class.new(%W(foo bar))
131
- worker.expects(:fork_and_watch).with("demo.test.foo").once
132
- worker.expects(:fork_and_watch).with("demo.test.bar").once
133
- silenced { worker.start(false) }
134
- end
135
-
136
- it "fork_and_watch should create a thread to fork and watch" do
137
- worker = @worker_class.new(%(foo))
138
- worker.expects(:create_thread).once.with("demo.test.foo")
139
- silenced { worker.start(false) }
140
- end
141
-
142
- it "fork_and_watch thread should wait with wait_for_process" do
143
- process_exit = stub('process_exit')
144
- process_exit.expects(:exitstatus).returns(99)
145
- worker = @worker_class.new(%(foo))
146
- worker.expects(:wait_for_process).with(12).returns([nil, process_exit])
147
-
148
- wc = @worker_class
149
- # TODO: Is there a best way do do this?
150
- worker.define_singleton_method :fork_it do
151
- wc.shutdown = true
152
- 12
153
- end
154
- def worker.create_thread(*args, &block); block.call(*args) end
155
-
156
- out = silenced(2) { worker.start(false) }
157
- refute_match(/Catastrophic failure/, out)
158
- end
159
-
160
- it "fork_and_watch thread should log a normal exit if exitstatus is 0 on shutdown" do
161
- process_exit = stub('process_exit', :exitstatus => 0)
162
- worker = @worker_class.new(%(foo))
163
- worker.expects(:wait_for_process).with(12).returns([nil, process_exit])
164
-
165
- wc = @worker_class
166
- worker.define_singleton_method :fork_it do
167
- wc.shutdown = true
168
- 12
169
- end
170
- def worker.create_thread(*args, &block); block.call(*args) end
171
- out = silenced(2) { worker.start(false) }
172
- assert_match(/Tube demo\.test\.foo exited normally\./, out)
173
- refute_match(/Catastrophic failure/, out)
174
- end
175
-
176
- describe "restarting a failed child" do
177
- before do
178
- @worker = @worker_class.new(%(foo))
179
- def @worker.create_thread(*args, &block); block.call(*args) end
180
- end
181
-
182
- # Runs fork_and_watch over children exiting with these codes, expecting it to sleep these delays in
183
- # turn, and returns what it logged
184
- def expect_restart_delays(codes, delays)
185
- wc = @worker_class
186
- left = codes.size
187
- @worker.define_singleton_method(:fork_it) { left -= 1; wc.shutdown = true if left.zero?; 12 }
188
- @worker.stubs(:wait_for_process).with(12).returns(*codes.map { |code| [12, stub(:exitstatus => code)] })
189
- restarts = sequence('restarts')
190
- delays.each { |delay| Kernel.expects(:sleep).with(delay).in_sequence(restarts) }
191
- silenced(2) { @worker.start(false) }
192
- end
193
-
194
- it "should wait longer before each fork while children keep failing, up to MAX_RESTART_DELAY" do
195
- expect_restart_delays([1] * 7, [1, 2, 4, 8, 16, 30])
196
- end
197
-
198
- it "should fork again at once after a recycled child and start the delays over" do
199
- expect_restart_delays([1, 1, 99, 1, 1], [1, 2, 1])
200
- end
201
-
202
- it "should log an error if exitstatus is != 99, and fork again" do
203
- out = expect_restart_delays([1, 99], [1])
204
- assert_match(/Catastrophic failure: tube demo\.test\.foo exited with code 1\. Forking it again in 1s\./, out)
205
- end
206
-
207
- it "should log a child that stops while shutting down without forking it again" do
208
- [nil, 1].each do |code| # killed by finish_forks, or exited with an error
209
- @worker_class.shutdown = false
210
- out = expect_restart_delays([code], [])
211
- assert_match(/Tube demo\.test\.foo stopped\./, out)
212
- refute_match(/Catastrophic failure|Forking it again/, out)
213
- end
214
- end
215
-
216
- it "should log the warning with a logger that has no warn method" do
217
- logged = []
218
- logger = Object.new
219
- logger.define_singleton_method(:info) { |msg| logged << [:info, msg] }
220
- logger.define_singleton_method(:error) { |msg| logged << [:error, msg] }
221
- Backburner.configure { |config| config.logger = logger }
222
- expect_restart_delays([0, 1], [1])
223
- assert_includes logged, [:error, "Tube demo.test.foo exited with code 0. Forking it again in 1s."]
224
- ensure
225
- Backburner.configure { |config| config.logger = nil }
226
- end
227
-
228
- it "should warn and wait before forking again after a child exits with code 0 outside a shutdown" do
229
- out = expect_restart_delays([0, 1], [1])
230
- assert_match(/Tube demo\.test\.foo exited with code 0\. Forking it again in 1s\./, out)
231
- refute_match(/exited normally/, out)
232
- end
233
-
234
- it "should start the delays over after a child that ran for MAX_RESTART_DELAY" do
235
- @worker.stubs(:monotonic_time).returns(0, 0.5, 1, 1.5, 4, 40, 41, 41.5)
236
- expect_restart_delays([1, 1, 1, 1], [1, 2, 1])
237
- end
238
- end # restarting a failed child
239
-
240
- describe "when beanstalkd is down while children start" do
241
- before do
242
- pick_beanstalkd_port
243
- start_beanstalkd
244
- @templogger = Templogger.new('/tmp')
245
- @beanstalk_url = Backburner.configuration.beanstalk_url
246
- Backburner.configure do |config|
247
- config.beanstalk_url = "beanstalk://127.0.0.1:#{@port}"
248
- config.logger = @templogger.logger
249
- end
250
- @worker_class.garbage_after = 1
251
- @watchers = []
252
- @ignore_forks = true
253
- end
254
-
255
- after do
256
- begin
257
- @worker_class.shutdown = true
258
- silenced do
259
- @worker_class.stop_forks
260
- Timeout::timeout(2) { sleep 0.1 while @worker_class.child_pids.length > 0 }
261
- @worker_class.kill_forks
262
- Timeout::timeout(2) { sleep 0.1 while @worker_class.child_pids.length > 0 }
263
- end
264
- ensure
265
- @watchers.each(&:kill) # once they've reaped the children: one waiting to fork again would wake up in a later test
266
- stop_beanstalkd
267
- Backburner.configure do |config|
268
- config.beanstalk_url = @beanstalk_url
269
- config.logger = nil
270
- end
271
- @templogger.close
272
- end
273
- end
274
-
275
- [1, 2].each do |threads|
276
- it "should back off instead of forking in a busy loop, and resume when it is back (#{threads} thread(s) per child)" do
277
- @worker_class.threads_number = threads
278
- worker = @worker_class.new('foo.bar.outage')
279
- forks = 0
280
- watchers = @watchers
281
- worker.define_singleton_method(:fork_tube) { |name| forks += 1; super(name) }
282
- worker.define_singleton_method(:create_thread) { |*args, &block| super(*args, &block).tap { |thread| watchers << thread } }
283
- outage_forks = nil
284
- silenced(20) do
285
- worker.start(false)
286
- sleep 0.5 # let the first child block in reserve
287
- stop_beanstalkd
288
- forks_before = forks
289
- sleep 4
290
- outage_forks = forks - forks_before
291
- start_beanstalkd
292
- @worker_class.enqueue TestJobNoopFork, [], :queue => 'foo.bar.outage'
293
- wait_for(10) { @templogger.body =~ /Completed TestJobNoopFork/ }
294
- end
295
- assert_operator outage_forks, :<=, 4, "forked #{outage_forks} children in 4 s"
296
- assert_match(/Completed TestJobNoopFork/, @templogger.body)
297
- end
298
- end
299
- end # beanstalkd down
300
-
301
- describe "fork_inner" do
302
-
303
- before do
304
- @worker_class.any_instance.expects(:coolest_exit).once
305
- end
306
-
307
- it "should watch just the channel it receive as argument" do
308
- worker = @worker_class.new(%(foo))
309
- @worker_class.expects(:threads_number).returns(1)
310
- worker.expects(:run_while_can).once
311
- silenced do
312
- worker.prepare
313
- worker.fork_inner('demo.test.bar')
314
- end
315
- assert_same_elements %W(demo.test.bar), worker.connection.tubes.watched.map(&:name)
316
- end
317
-
318
- it "should not create threads if the number of threads is 1" do
319
- worker = @worker_class.new(%(foo))
320
- @worker_class.expects(:threads_number).returns(1)
321
- worker.expects(:run_while_can).once
322
- worker.expects(:create_thread).never
323
- silenced do
324
- worker.prepare
325
- worker.fork_inner('demo.test.foo')
326
- end
327
- end
328
-
329
- it "should create threads if the number of threads is > 1" do
330
- worker = @worker_class.new(%(foo))
331
- @worker_class.expects(:threads_number).returns(5)
332
- worker.expects(:create_thread).times(5).returns(Thread.new {})
333
- silenced do
334
- worker.prepare
335
- worker.fork_inner('demo.test.foo')
336
- end
337
- end
338
-
339
- it "should create threads that call run_while_can" do
340
- worker = @worker_class.new(%(foo))
341
- @worker_class.expects(:threads_number).returns(5)
342
- worker.expects(:run_while_can).times(5)
343
- def worker.create_thread(*args, &block); super(*args, &block).tap(&:join) end
344
- silenced do
345
- worker.prepare
346
- worker.fork_inner('demo.test.foo')
347
- end
348
- end
349
-
350
- it "should create a connection for each thread" do
351
- name = 'demo.test.foo'
352
- num_threads = 3
353
-
354
- worker = @worker_class.new(%(foo))
355
- @worker_class.expects(:threads_number).returns(num_threads)
356
-
357
- fake_conn = Struct.new(:num)
358
- invocations = Array(1..num_threads).map do |i|
359
- conn = fake_conn.new(i)
360
- conn.expects(:close)
361
- conn
362
- end
363
- Backburner::Connection.expects(:new).times(num_threads).returns(*invocations)
364
-
365
- # ensure each invocation of run_while_can is with a different connection
366
- num_conns = states('num_conns').starts_as(0)
367
- invocations.each do |conn|
368
- worker.expects(:watch_tube).with(name, conn)
369
- worker.expects(:run_while_can).with(conn).when(num_conns.is(conn.num-1)).then(num_conns.is(conn.num))
370
- end
371
-
372
- def worker.create_thread(*args, &block); super(*args, &block).tap(&:join) end
373
- silenced do
374
- worker.prepare
375
- worker.fork_inner(name)
376
- end
377
-
378
- assert_equal(num_threads, num_conns.current_state)
379
- end
380
-
381
- it "should exit once its own threads finish, even while other threads live on" do
382
- worker = @worker_class.new(%(foo))
383
- @worker_class.expects(:threads_number).returns(2)
384
- strays, inner = [], nil
385
- # Threads a job leaves running, like the one the timeout gem keeps since Ruby 3.2
386
- worker.define_singleton_method(:run_while_can) { |conn| strays << Thread.new { sleep } }
387
- silenced do
388
- worker.prepare
389
- inner = Thread.new { worker.fork_inner('demo.test.foo') }
390
- assert inner.join(2), "the child still waited for threads that weren't its own"
391
- end
392
- ensure
393
- inner.kill if inner
394
- strays.each(&:kill) if strays
395
- end
396
-
397
- it "should set @garbage_after, @threads_number and set retries if needed" do
398
- worker = @worker_class.new(%W(foo1 foo2:10 foo3:20:30 foo4:40:50:60))
399
- default_threads = 1
400
- default_garbage = 5
401
- default_retries = 100
402
- @worker_class.expects(:threads_number).times(1).returns(default_threads)
403
- @worker_class.expects(:garbage_after).times(2).returns(default_garbage)
404
- @worker_class.any_instance.expects(:coolest_exit).times(3)
405
- Backburner.configuration.max_job_retries = default_retries
406
-
407
- worker.expects(:create_thread).times(70).returns(Thread.new {})
408
- worker.expects(:run_while_can).once
409
-
410
- silenced do
411
- worker.prepare
412
- worker.fork_inner('demo.test.foo1')
413
- end
414
-
415
- assert_equal worker.instance_variable_get("@threads_number"), default_threads
416
- assert_equal worker.instance_variable_get("@garbage_after"), default_garbage
417
- assert_equal Backburner.configuration.max_job_retries, default_retries
418
-
419
- silenced do
420
- worker.fork_inner('demo.test.foo2')
421
- end
422
-
423
- assert_equal worker.instance_variable_get("@threads_number"), 10
424
- assert_equal worker.instance_variable_get("@garbage_after"), default_garbage
425
- assert_equal Backburner.configuration.max_job_retries, default_retries
426
-
427
- silenced do
428
- worker.fork_inner('demo.test.foo3')
429
- end
430
-
431
- assert_equal worker.instance_variable_get("@threads_number"), 20
432
- assert_equal worker.instance_variable_get("@garbage_after"), 30
433
- assert_equal Backburner.configuration.max_job_retries, default_retries
434
-
435
- silenced do
436
- worker.fork_inner('demo.test.foo4')
437
- end
438
-
439
- assert_equal worker.instance_variable_get("@threads_number"), 40
440
- assert_equal worker.instance_variable_get("@garbage_after"), 50
441
- assert_equal Backburner.configuration.max_job_retries, 60
442
- end
443
-
444
- end
445
-
446
- describe "cleanup on parent" do
447
-
448
- it "child_pids should return a list of alive children pids" do
449
- worker = @worker_class.new(%W(foo))
450
- Kernel.expects(:fork).once.returns(12345)
451
- Process.expects(:kill).with(0, 12345).once
452
- Process.expects(:pid).once.returns(12346)
453
- assert_equal [], @worker_class.child_pids
454
- worker.fork_it {}
455
- child_pids = @worker_class.child_pids
456
- assert_equal [12345], child_pids
457
- child_pids.clear
458
- end
459
-
460
- it "child_pids should return an empty array if is_child" do
461
- Process.expects(:pid).never
462
- @worker_class.is_child = true
463
- @worker_class.child_pids << 12345
464
- assert_equal [], @worker_class.child_pids
465
- end
466
-
467
- it "stop_forks should send a SIGTERM for every child" do
468
- Process.expects(:pid).returns(12346).at_least(1)
469
- Process.expects(:kill).with(0, 12345).at_least(1)
470
- Process.expects(:kill).with(0, 12347).at_least(1)
471
- Process.expects(:kill).with("SIGTERM", 12345)
472
- Process.expects(:kill).with("SIGTERM", 12347)
473
- @worker_class.child_pids << 12345
474
- @worker_class.child_pids << 12347
475
- assert_equal [12345, 12347], @worker_class.child_pids
476
- @worker_class.stop_forks
477
- @worker_class.child_pids.clear
478
- end
479
-
480
- it "kill_forks should send a SIGKILL for every child" do
481
- Process.expects(:pid).returns(12346).at_least(1)
482
- Process.expects(:kill).with(0, 12345).at_least(1)
483
- Process.expects(:kill).with(0, 12347).at_least(1)
484
- Process.expects(:kill).with("SIGKILL", 12345)
485
- Process.expects(:kill).with("SIGKILL", 12347)
486
- @worker_class.child_pids << 12345
487
- @worker_class.child_pids << 12347
488
- assert_equal [12345, 12347], @worker_class.child_pids
489
- @worker_class.kill_forks
490
- @worker_class.child_pids.clear
491
- end
492
-
493
- it "finish_forks should call stop_forks, kill_forks and Process.waitall" do
494
- Process.expects(:pid).returns(12346).at_least(1)
495
- Process.expects(:kill).with(0, 12345).at_least(1)
496
- Process.expects(:kill).with(0, 12347).at_least(1)
497
- Process.expects(:kill).with("SIGTERM", 12345)
498
- Process.expects(:kill).with("SIGTERM", 12347)
499
- Process.expects(:kill).with("SIGKILL", 12345)
500
- Process.expects(:kill).with("SIGKILL", 12347)
501
- Kernel.expects(:sleep).with(1)
502
- Process.expects(:waitall)
503
- @worker_class.child_pids << 12345
504
- @worker_class.child_pids << 12347
505
- assert_equal [12345, 12347], @worker_class.child_pids
506
- silenced do
507
- @worker_class.finish_forks
508
- end
509
- @worker_class.child_pids.clear
510
- end
511
-
512
- it "finish_forks should not do anything if is_child" do
513
- @worker_class.expects(:stop_forks).never
514
- @worker_class.is_child = true
515
- @worker_class.child_pids << 12345
516
- silenced do
517
- @worker_class.finish_forks
518
- end
519
- end
520
-
521
- end # cleanup on parent
522
-
523
- describe "a child exiting" do
524
- before do
525
- Backburner::Worker.stubs(:gc_disabled_on_exit?).returns(true) # as on Ruby 3.3
526
- end
527
-
528
- [:return, :raise, :shutdown].each do |how|
529
- it "should disable the GC before the interpreter's teardown (#{how})" do
530
- worker = @worker_class.new(%W(foo))
531
- gc_disabled = gc_disabled_at_child_exit?(worker, how) do |body|
532
- worker.wait_for_process(worker.fork_it(&body))
533
- end
534
- assert gc_disabled, "the child's GC was still enabled when it exited"
535
- end
536
- end
537
-
538
- it "should leave the GC enabled on a Ruby other than 3.3" do
539
- Backburner::Worker.stubs(:gc_disabled_on_exit?).returns(false)
540
- worker = @worker_class.new(%W(foo))
541
- gc_disabled = gc_disabled_at_child_exit?(worker, :shutdown) do |body|
542
- worker.wait_for_process(worker.fork_it(&body))
543
- end
544
- refute gc_disabled, "the child's GC was disabled when it exited"
545
- end
546
-
547
- describe "while running several threads" do
548
- before do
549
- @worker_class.threads_number = 2
550
- @worker_class.garbage_after = nil
551
- end
552
-
553
- after do
554
- @worker_class.threads_number = 1
555
- @worker_class.garbage_after = 1
556
- end
557
-
558
- it "should stop its threads before its at_exit handlers run on a TERM, so they don't work jobs with the GC off" do
559
- worker = @worker_class.new(%W(foo))
560
- reader, writer = IO.pipe
561
- pid = worker.fork_it do
562
- reader.close
563
- $stdout.reopen(File::NULL)
564
- $stderr.reopen(File::NULL)
565
- $jobs_worked = 0
566
- worker.define_singleton_method(:run_while_can) { |*| loop { $jobs_worked += 1; sleep 0.01 } } # jobs keep coming
567
- # An app's slow at_exit handler (an error reporter flushing, say)
568
- at_exit { sleep 0.05; before = $jobs_worked; sleep 0.3; writer.write(($jobs_worked - before).to_s); writer.close }
569
- Thread.new { sleep 0.3; Process.kill(:TERM, Process.pid) }
570
- worker.fork_inner("demo.test.foo")
571
- end
572
- writer.close
573
- worker.wait_for_process(pid)
574
- assert_equal "0", reader.read, "the child's threads kept working jobs while its at_exit handlers ran"
575
- ensure
576
- reader.close if reader && !reader.closed?
577
- writer.close if writer && !writer.closed?
578
- end
579
- end
580
- end
581
-
582
- describe "practical tests" do
583
-
584
- before do
585
- @templogger = Templogger.new('/tmp')
586
- Backburner.configure { |config| config.logger = @templogger.logger }
587
- $worker_test_count = 0
588
- $worker_success = false
589
- $worker_raise = false
590
- clear_jobs!('response')
591
- clear_jobs!('foo.bar.1', 'foo.bar.2', 'foo.bar.3', 'foo.bar.4', 'foo.bar.5', 'foo.bar.6', 'foo.bar.7', 'foo.bar.8')
592
- @worker_class.threads_number = 1
593
- @worker_class.garbage_after = 10
594
-
595
- silenced do
596
- @response_worker = @worker_class.new('response')
597
- @response_worker.watch_tube('demo.test.response')
598
- end
599
-
600
- @ignore_forks = true
601
- end
602
-
603
- after do
604
- @templogger.close
605
- clear_jobs!('response')
606
- clear_jobs!('foo.bar.1', 'foo.bar.2', 'foo.bar.3', 'foo.bar.4', 'foo.bar.5', 'foo.bar.6', 'foo.bar.7', 'foo.bar.8')
607
- @worker_class.threads_number = 1
608
- @worker_class.shutdown = true
609
- silenced do
610
- @worker_class.stop_forks
611
- Timeout::timeout(2) { sleep 0.1 while @worker_class.child_pids.length > 0 }
612
- @worker_class.kill_forks
613
- Timeout::timeout(2) { sleep 0.1 while @worker_class.child_pids.length > 0 }
614
- end
615
- end
616
-
617
- it "should work an enqueued job" do
618
- @worker = @worker_class.new('foo.bar.1')
619
- @worker.start(false)
620
- @worker_class.enqueue TestJobFork, [1, 2], :queue => "foo.bar.1"
621
-
622
- silenced do
623
- @templogger.wait_for_match(/Completed TestJobFork/m)
624
- @response_worker.work_one_job
625
- end
626
- assert_equal 3, $worker_test_count
627
- end # enqueue
628
-
629
- it "should work for an async job" do
630
- @worker = @worker_class.new('foo.bar.2')
631
- @worker.start(false)
632
- TestAsyncJobFork.async(:queue => 'foo.bar.2').foo(3, 5)
633
- silenced(2) do
634
- @templogger.wait_for_match(/Completed TestAsyncJobFork/m)
635
- @response_worker.work_one_job
636
- end
637
- assert_equal 15, $worker_test_count
638
- end # async
639
-
640
- it "should fail quietly if there's an argument error" do
641
- @worker = @worker_class.new('foo.bar.3')
642
- @worker.start(false)
643
- @worker_class.enqueue TestJobFork, ["bam", "foo", "bar"], :queue => "foo.bar.3"
644
- silenced(5) do
645
- @templogger.wait_for_match(/Finished TestJobFork.*attempt 1 of 1/m)
646
- end
647
- assert_match(/Exception ArgumentError/, @templogger.body)
648
- assert_equal 0, $worker_test_count
649
- end # fail, argument
650
-
651
- it "should support retrying jobs and burying" do
652
- Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
653
- @worker = @worker_class.new('foo.bar.4')
654
- @worker.start(false)
655
- @worker_class.enqueue TestRetryJobFork, ["bam", "foo"], :queue => 'foo.bar.4'
656
- silenced(2) do
657
- @templogger.wait_for_match(/Finished TestRetryJobFork.*attempt 2 of 2/m)
658
- 2.times { @response_worker.work_one_job }
659
- end
660
- assert_equal 2, $worker_test_count
661
- assert_equal false, $worker_success
662
- end # retry, bury
663
-
664
- it "should support retrying jobs and succeeds" do
665
- Backburner.configure { |config| config.max_job_retries = 2; config.retry_delay = 0 }
666
- @worker = @worker_class.new('foo.bar.5')
667
- @worker.start(false)
668
- @worker_class.enqueue TestRetryJobFork, ["bam", "foo"], :queue => 'foo.bar.5'
669
- silenced(2) do
670
- @templogger.wait_for_match(/Completed TestRetryJobFork/m)
671
- 3.times { @response_worker.work_one_job }
672
- end
673
- assert_equal 3, $worker_test_count
674
- assert_equal true, $worker_success
675
- end # retrying, succeeds
676
-
677
- it "should support a multithreaded worker without deadlocks" do
678
- num_threads = 15
679
- num_jobs = 8
680
- num_jobs.times do |i|
681
- @worker_class.enqueue TestJobMultithreadFork, [6,2], :queue => 'foo.bar.6'
682
- end
683
-
684
- @worker_class.threads_number = num_threads
685
- @worker = @worker_class.new('foo.bar.6')
686
- @worker.start(false)
687
-
688
- silenced do
689
- @templogger.wait_for_match(/Completed TestJobMultithreadFork/m)
690
- num_jobs.times { @response_worker.work_one_job }
691
- end
692
- assert_equal num_jobs, $worker_test_count
693
- end # multithreaded
694
-
695
- it "should give each forked tube its own connection when running one thread per tube" do
696
- num_jobs = 5
697
- tubes = %w(foo.bar.7 foo.bar.8)
698
- @worker = @worker_class.new(tubes)
699
- @worker.start(false)
700
- tubes.each do |tube|
701
- num_jobs.times { @worker_class.enqueue TestJobNoopFork, [], :queue => tube }
702
- end
703
-
704
- completed = lambda { @templogger.body.scan(/Completed TestJobNoopFork/).size }
705
- deadline = Time.now + 10
706
- sleep 0.1 until completed.call >= tubes.size * num_jobs || Time.now > deadline
707
-
708
- assert_equal tubes.size * num_jobs, completed.call, @templogger.body
709
- conn = beanstalk_connection
710
- tubes.each do |tube|
711
- stats = conn.tubes["demo.test.#{tube}"].stats
712
- assert_equal 0, stats.current_jobs_ready + stats.current_jobs_reserved, "jobs left in #{tube}"
713
- end
714
- conn.close
715
- end # one thread per tube
716
-
717
- it "should release a job whose class can't be found and keep its child working" do
718
- clear_jobs!('foo.bar.9')
719
- conn = beanstalk_connection
720
- conn.tubes['demo.test.foo.bar.9'].put(Backburner.configuration.job_serializer_proc.call(:class => 'TestRemovedJobFork', :args => []), :ttr => 30)
721
- @worker = @worker_class.new('foo.bar.9')
722
- @worker.start(false)
723
- @worker_class.enqueue TestJobNoopFork, [], :queue => 'foo.bar.9'
724
-
725
- silenced(5) { @templogger.wait_for_match(/Completed TestJobNoopFork/m) }
726
- assert_match(/TestRemovedJobFork: class not found; released for 30s/, @templogger.body)
727
- refute_match(/Catastrophic failure/, @templogger.body)
728
- stats = conn.tubes['demo.test.foo.bar.9'].stats
729
- assert_equal [0, 1], [stats.current_jobs_buried, stats.current_jobs_delayed]
730
- ensure
731
- conn.close if conn
732
- clear_jobs!('foo.bar.9')
733
- end # unknown class
734
-
735
- it "should recycle a child running several threads after garbage_after jobs and keep working" do
736
- clear_jobs!('foo.bar.10')
737
- @worker_class.threads_number = 2
738
- @worker_class.garbage_after = 2
739
- @worker = @worker_class.new('foo.bar.10')
740
- @worker.start(false)
741
- 6.times { @worker_class.enqueue TestJobNoopFork, [], :queue => 'foo.bar.10' }
742
-
743
- completed = lambda { @templogger.body.scan(/Completed TestJobNoopFork/).size }
744
- wait_for(10) { completed.call >= 6 }
745
- assert_equal 6, completed.call, @templogger.body
746
- ensure
747
- clear_jobs!('foo.bar.10')
748
- end # recycled multithreaded child
749
- end # practical tests
750
- end # forking and threading
751
- end # Backburner::Workers::ThreadsOnFork module