backburner-allq 1.0.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +29 -0
  4. data/CHANGELOG.md +133 -0
  5. data/CONTRIBUTING.md +37 -0
  6. data/Gemfile +4 -0
  7. data/HOOKS.md +99 -0
  8. data/LICENSE +22 -0
  9. data/README.md +658 -0
  10. data/Rakefile +17 -0
  11. data/TODO +4 -0
  12. data/backburner-allq.gemspec +26 -0
  13. data/bin/backburner +7 -0
  14. data/circle.yml +3 -0
  15. data/deploy.sh +3 -0
  16. data/examples/custom.rb +25 -0
  17. data/examples/demo.rb +60 -0
  18. data/examples/god.rb +46 -0
  19. data/examples/hooked.rb +87 -0
  20. data/examples/retried.rb +31 -0
  21. data/examples/simple.rb +43 -0
  22. data/examples/stress.rb +31 -0
  23. data/lib/backburner.rb +75 -0
  24. data/lib/backburner/allq_wrapper.rb +317 -0
  25. data/lib/backburner/async_proxy.rb +25 -0
  26. data/lib/backburner/cli.rb +53 -0
  27. data/lib/backburner/configuration.rb +48 -0
  28. data/lib/backburner/connection.rb +157 -0
  29. data/lib/backburner/helpers.rb +193 -0
  30. data/lib/backburner/hooks.rb +53 -0
  31. data/lib/backburner/job.rb +118 -0
  32. data/lib/backburner/logger.rb +53 -0
  33. data/lib/backburner/performable.rb +95 -0
  34. data/lib/backburner/queue.rb +145 -0
  35. data/lib/backburner/tasks.rb +54 -0
  36. data/lib/backburner/version.rb +3 -0
  37. data/lib/backburner/worker.rb +221 -0
  38. data/lib/backburner/workers/forking.rb +52 -0
  39. data/lib/backburner/workers/simple.rb +29 -0
  40. data/lib/backburner/workers/threading.rb +163 -0
  41. data/lib/backburner/workers/threads_on_fork.rb +263 -0
  42. data/test/async_proxy_test.rb +36 -0
  43. data/test/back_burner_test.rb +88 -0
  44. data/test/connection_test.rb +179 -0
  45. data/test/fixtures/hooked.rb +122 -0
  46. data/test/fixtures/test_fork_jobs.rb +72 -0
  47. data/test/fixtures/test_forking_jobs.rb +56 -0
  48. data/test/fixtures/test_jobs.rb +87 -0
  49. data/test/fixtures/test_queue_settings.rb +14 -0
  50. data/test/helpers/templogger.rb +22 -0
  51. data/test/helpers_test.rb +278 -0
  52. data/test/hooks_test.rb +112 -0
  53. data/test/job_test.rb +185 -0
  54. data/test/logger_test.rb +44 -0
  55. data/test/performable_test.rb +88 -0
  56. data/test/queue_test.rb +69 -0
  57. data/test/test_helper.rb +128 -0
  58. data/test/worker_test.rb +157 -0
  59. data/test/workers/forking_worker_test.rb +181 -0
  60. data/test/workers/simple_worker_test.rb +350 -0
  61. data/test/workers/threading_worker_test.rb +104 -0
  62. data/test/workers/threads_on_fork_worker_test.rb +484 -0
  63. metadata +217 -0
@@ -0,0 +1,350 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+ require File.expand_path('../../fixtures/test_jobs', __FILE__)
3
+ require File.expand_path('../../fixtures/hooked', __FILE__)
4
+
5
+ describe "Backburner::Workers::Simple module" do
6
+ before do
7
+ Backburner.default_queues.clear
8
+ @worker_class = Backburner::Workers::Simple
9
+ end
10
+
11
+ describe "for prepare method" do
12
+ it "should make tube names array always unique to avoid duplication" do
13
+ worker = @worker_class.new(["foo", "demo.test.foo"])
14
+ capture_stdout { worker.prepare }
15
+ assert_equal ["demo.test.foo"], worker.tube_names
16
+ end
17
+
18
+ it "should watch specified tubes" do
19
+ worker = @worker_class.new(["foo", "bar"])
20
+ out = capture_stdout { worker.prepare }
21
+ assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
22
+ assert_same_elements ["demo.test.foo", "demo.test.bar"], worker.connection.tubes.watched.map(&:name)
23
+ assert_match(/demo\.test\.foo/, out)
24
+ end # multiple
25
+
26
+ it "should watch single tube" do
27
+ worker = @worker_class.new("foo")
28
+ out = capture_stdout { worker.prepare }
29
+ assert_equal ["demo.test.foo"], worker.tube_names
30
+ assert_same_elements ["demo.test.foo"], worker.connection.tubes.watched.map(&:name)
31
+ assert_match(/demo\.test\.foo/, out)
32
+ end # single
33
+
34
+ it "should respect default_queues settings" do
35
+ Backburner.default_queues.concat(["foo", "bar"])
36
+ worker = @worker_class.new
37
+ out = capture_stdout { worker.prepare }
38
+ assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
39
+ assert_same_elements ["demo.test.foo", "demo.test.bar"], worker.connection.tubes.watched.map(&:name)
40
+ assert_match(/demo\.test\.foo/, out)
41
+ end
42
+
43
+ it "should assign based on all tubes" do
44
+ @worker_class.any_instance.expects(:all_existing_queues).once.returns("bar")
45
+ worker = @worker_class.new
46
+ out = capture_stdout { worker.prepare }
47
+ assert_equal ["demo.test.bar"], worker.tube_names
48
+ assert_same_elements ["demo.test.bar"], worker.connection.tubes.watched.map(&:name)
49
+ assert_match(/demo\.test\.bar/, out)
50
+ end # all assign
51
+
52
+ it "should properly retrieve all tubes" do
53
+ worker = @worker_class.new
54
+ out = capture_stdout { worker.prepare }
55
+ assert_contains worker.tube_names, "demo.test.backburner-jobs"
56
+ assert_contains worker.connection.tubes.watched.map(&:name), "demo.test.backburner-jobs"
57
+ assert_match(/demo\.test\.backburner-jobs/, out)
58
+ end # all read
59
+ end # prepare
60
+
61
+ describe "for work_one_job method" do
62
+ before do
63
+ $worker_test_count = 0
64
+ $worker_success = false
65
+ end
66
+
67
+ it "should work a plain enqueued job" do
68
+ clear_jobs!("foo.bar")
69
+ @worker_class.enqueue TestPlainJob, [1, 2], :queue => "foo.bar"
70
+ silenced(2) do
71
+ worker = @worker_class.new('foo.bar')
72
+ worker.prepare
73
+ worker.work_one_job
74
+ end
75
+ assert_equal 4, $worker_test_count
76
+ end # plain enqueue
77
+
78
+ it "should work an enqueued job" do
79
+ clear_jobs!("foo.bar")
80
+ @worker_class.enqueue TestJob, [1, 2], :queue => "foo.bar"
81
+ silenced(2) do
82
+ worker = @worker_class.new('foo.bar')
83
+ worker.prepare
84
+ worker.work_one_job
85
+ end
86
+ assert_equal 3, $worker_test_count
87
+ end # enqueue
88
+
89
+ it "should fail quietly if there's an argument error" do
90
+ clear_jobs!("foo.bar")
91
+ @worker_class.enqueue TestJob, ["bam", "foo", "bar"], :queue => "foo.bar"
92
+ out = silenced(2) do
93
+ worker = @worker_class.new('foo.bar')
94
+ worker.prepare
95
+ worker.work_one_job
96
+ end
97
+ assert_match(/Exception ArgumentError/, out)
98
+ assert_equal 0, $worker_test_count
99
+ end # fail, argument
100
+
101
+ it "should work an enqueued failing job" do
102
+ # NB: The #bury expectation below leaves the job in the queue (as reserved!)
103
+ # since bury is never actually called on the task. Therefore, clear_jobs!()
104
+ # can't remove it which can break a lot of things depending on the order the
105
+ # tests are run. So we ensure that it's using a unique queue name. Mocha
106
+ # lacks expectations with proxies (where we could actually call bury)
107
+ clear_jobs!('foo.bar.failed')
108
+ @worker_class.enqueue TestFailJob, [1, 2], :queue => 'foo.bar.failed'
109
+ Backburner::Job.any_instance.expects(:bury).once
110
+ out = silenced(2) do
111
+ worker = @worker_class.new('foo.bar.failed')
112
+ worker.prepare
113
+ worker.work_one_job
114
+ end
115
+ assert_match(/Exception RuntimeError/, out)
116
+ assert_equal 0, $worker_test_count
117
+ end # fail, runtime error
118
+
119
+ it "should work an invalid job parsed" do
120
+ Beaneater::Tubes.any_instance.expects(:reserve).returns(stub(:body => "{%$^}", :bury => true))
121
+ out = silenced(2) do
122
+ worker = @worker_class.new('foo.bar')
123
+ worker.prepare
124
+ worker.work_one_job
125
+ end
126
+ assert_match(/Exception Backburner::Job::JobFormatInvalid/, out)
127
+ assert_equal 0, $worker_test_count
128
+ end # fail, runtime error
129
+
130
+ it "should work for an async job" do
131
+ clear_jobs!('foo.bar')
132
+ TestAsyncJob.async(:queue => 'foo.bar').foo(3, 5)
133
+ silenced(2) do
134
+ worker = @worker_class.new('foo.bar')
135
+ worker.prepare
136
+ worker.work_one_job
137
+ end
138
+ assert_equal 15, $worker_test_count
139
+ end # async
140
+
141
+ it "should support retrying jobs and burying" do
142
+ clear_jobs!('foo.bar')
143
+ Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
144
+ @worker_class.enqueue TestRetryJob, ["bam", "foo"], :queue => 'foo.bar'
145
+ out = []
146
+ 2.times do
147
+ out << silenced(2) do
148
+ worker = @worker_class.new('foo.bar')
149
+ worker.prepare
150
+ worker.work_one_job
151
+ end
152
+ end
153
+ assert_match(/attempt 1 of 2, retrying/, out.first)
154
+ assert_match(/Finished TestRetryJob/m, out.last)
155
+ assert_match(/attempt 2 of 2, burying/m, out.last)
156
+ assert_equal 2, $worker_test_count
157
+ assert_equal false, $worker_success
158
+ end # retry, bury
159
+
160
+ it "should support retrying jobs and succeeds" do
161
+ clear_jobs!('foo.bar')
162
+ Backburner.configure { |config| config.max_job_retries = 2; config.retry_delay = 0 }
163
+ @worker_class.enqueue TestRetryJob, ["bam", "foo"], :queue => 'foo.bar'
164
+ out = []
165
+ 3.times do
166
+ out << silenced(2) do
167
+ worker = @worker_class.new('foo.bar')
168
+ worker.prepare
169
+ worker.work_one_job
170
+ end
171
+ end
172
+ assert_match(/attempt 1 of 3, retrying/, out.first)
173
+ assert_match(/attempt 2 of 3, retrying/, out[1])
174
+ assert_match(/Completed TestRetryJob/m, out.last)
175
+ refute_match(/failed/, out.last)
176
+ assert_equal 3, $worker_test_count
177
+ assert_equal true, $worker_success
178
+ end # retrying, succeeds
179
+
180
+ it "should back off retries exponentially" do
181
+ max_job_retries = 3
182
+ clear_jobs!('foo.bar')
183
+ Backburner.configure do |config|
184
+ config.max_job_retries = max_job_retries
185
+ config.retry_delay = 0
186
+ #config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) } # default retry_delay_proc
187
+ end
188
+ @worker_class.enqueue TestConfigurableRetryJob, [max_job_retries], :queue => 'foo.bar'
189
+ out = []
190
+ (max_job_retries + 1).times do
191
+ out << silenced(10) do
192
+ worker = @worker_class.new('foo.bar')
193
+ worker.prepare
194
+ worker.work_one_job
195
+ end
196
+ end
197
+ assert_match(/attempt 1 of 4, retrying in 0/, out.first)
198
+ assert_match(/attempt 2 of 4, retrying in 1/, out[1])
199
+ assert_match(/attempt 3 of 4, retrying in 8/, out[2])
200
+ assert_match(/Completed TestConfigurableRetryJob/m, out.last)
201
+ refute_match(/failed/, out.last)
202
+ assert_equal 4, $worker_test_count
203
+ assert_equal true, $worker_success
204
+ end
205
+
206
+ it "should allow configurable back off retry delays" do
207
+ max_job_retries = 3
208
+ clear_jobs!('foo.bar')
209
+ Backburner.configure do |config|
210
+ config.max_job_retries = max_job_retries
211
+ config.retry_delay = 0
212
+ config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 2) }
213
+ end
214
+ @worker_class.enqueue TestConfigurableRetryJob, [max_job_retries], :queue => 'foo.bar'
215
+ out = []
216
+ (max_job_retries + 1).times do
217
+ out << silenced(5) do
218
+ worker = @worker_class.new('foo.bar')
219
+ worker.prepare
220
+ worker.work_one_job
221
+ end
222
+ end
223
+ assert_match(/attempt 1 of 4, retrying in 0/, out.first)
224
+ assert_match(/attempt 2 of 4, retrying in 1/, out[1])
225
+ assert_match(/attempt 3 of 4, retrying in 4/, out[2])
226
+ assert_match(/Completed TestConfigurableRetryJob/m, out.last)
227
+ refute_match(/failed/, out.last)
228
+ assert_equal 4, $worker_test_count
229
+ assert_equal true, $worker_success
230
+ end
231
+
232
+ it "should allow queue override of retries" do
233
+ max_job_retries = TestRetryWithQueueOverridesJob.queue_max_job_retries
234
+ clear_jobs!('foo.bar')
235
+ Backburner.configure do |config|
236
+ # Config should be overridden by queue overrides
237
+ config.max_job_retries = 20
238
+ config.retry_delay = 60
239
+ #config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) } # default retry_delay_proc
240
+ end
241
+ @worker_class.enqueue TestRetryWithQueueOverridesJob, [max_job_retries], :queue => 'foo.bar'
242
+ out = []
243
+ (max_job_retries + 1).times do
244
+ out << silenced(5) do
245
+ worker = @worker_class.new('foo.bar')
246
+ worker.prepare
247
+ worker.work_one_job
248
+ end
249
+ end
250
+ assert_match(/attempt 1 of 4, retrying in 0/, out.first)
251
+ assert_match(/attempt 2 of 4, retrying in 1/, out[1])
252
+ assert_match(/attempt 3 of 4, retrying in 4/, out[2])
253
+ assert_match(/Completed TestRetryWithQueueOverridesJob/m, out.last)
254
+ refute_match(/failed/, out.last)
255
+ assert_equal 4, $worker_test_count
256
+ assert_equal true, $worker_success
257
+ end
258
+
259
+ it "should support event hooks without retry" do
260
+ $hooked_fail_count = 0
261
+ clear_jobs!('foo.bar.events')
262
+ out = silenced(2) do
263
+ HookedObjectSuccess.async(:queue => 'foo.bar.events').foo(5)
264
+ worker = @worker_class.new('foo.bar.events')
265
+ worker.prepare
266
+ worker.work_one_job
267
+ end
268
+ assert_match(/before_enqueue.*after_enqueue.*Working 1 queues/m, out)
269
+ assert_match(/!!before_enqueue_bar!! \[nil, :foo, 5\]/, out)
270
+ assert_match(/!!after_enqueue_bar!! \[nil, :foo, 5\]/, out)
271
+ assert_match(/!!before_perform_foo!! \[nil, "foo", 5\]/, out)
272
+ assert_match(/!!BEGIN around_perform_bar!! \[nil, "foo", 5\]/, out)
273
+ assert_match(/!!BEGIN around_perform_cat!! \[nil, "foo", 5\]/, out)
274
+ assert_match(/!!on_failure_foo!!.*HookFailError/, out)
275
+ assert_match(/!!on_bury_foo!! \[nil, "foo", 5\]/, out)
276
+ assert_match(/attempt 1 of 1, burying/, out)
277
+ end # event hooks, no retry
278
+
279
+ it "should support event hooks with retry" do
280
+ $hooked_fail_count = 0
281
+ clear_jobs!('foo.bar.events.retry')
282
+ Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
283
+ out = silenced(2) do
284
+ HookedObjectSuccess.async(:queue => 'foo.bar.events.retry').foo(5)
285
+ worker = @worker_class.new('foo.bar.events.retry')
286
+ worker.prepare
287
+ 2.times do
288
+ worker.work_one_job
289
+ end
290
+ end
291
+ assert_match(/before_enqueue.*after_enqueue.*Working 1 queues/m, out)
292
+ assert_match(/!!before_enqueue_bar!! \[nil, :foo, 5\]/, out)
293
+ assert_match(/!!after_enqueue_bar!! \[nil, :foo, 5\]/, out)
294
+ assert_match(/!!before_perform_foo!! \[nil, "foo", 5\]/, out)
295
+ assert_match(/!!BEGIN around_perform_bar!! \[nil, "foo", 5\]/, out)
296
+ assert_match(/!!BEGIN around_perform_cat!! \[nil, "foo", 5\]/, out)
297
+ assert_match(/!!on_failure_foo!!.*HookFailError/, out)
298
+ assert_match(/!!on_failure_foo!!.*retrying.*around_perform_bar.*around_perform_cat/m, out)
299
+ assert_match(/!!on_retry_foo!! 1 0 \[nil, "foo", 5\]/, out)
300
+ assert_match(/attempt 1 of 2, retrying/, out)
301
+ assert_match(/!!before_perform_foo!! \[nil, "foo", 5\]/, out)
302
+ assert_match(/!!END around_perform_bar!! \[nil, "foo", 5\]/, out)
303
+ assert_match(/!!END around_perform_cat!! \[nil, "foo", 5\]/, out)
304
+ assert_match(/!!after_perform_foo!! \[nil, "foo", 5\]/, out)
305
+ assert_match(/Finished HookedObjectSuccess/, out)
306
+ end # event hooks, with retry
307
+
308
+ it "should support event hooks with stopping enqueue" do
309
+ $hooked_fail_count = 0
310
+ worker = @worker_class.new('foo.bar.events.retry2')
311
+ clear_jobs!('foo.bar.events.retry2')
312
+ silenced(2) do
313
+ HookedObjectBeforeEnqueueFail.async(:queue => 'foo.bar.events.retry2').foo(5)
314
+ end
315
+ expanded_tube = [Backburner.configuration.tube_namespace, 'foo.bar.events.retry2'].join(".")
316
+ assert_nil worker.connection.tubes[expanded_tube].peek(:ready)
317
+ end # stopping enqueue
318
+
319
+ it "should support event hooks with stopping perform" do
320
+ $hooked_fail_count = 0
321
+ clear_jobs!('foo.bar.events.retry3')
322
+ [Backburner.configuration.tube_namespace, 'foo.bar.events.retry3'].join(".")
323
+ out = silenced(2) do
324
+ HookedObjectBeforePerformFail.async(:queue => 'foo.bar.events.retry3').foo(10)
325
+ worker = @worker_class.new('foo.bar.events.retry3')
326
+ worker.prepare
327
+ worker.work_one_job
328
+ end
329
+ assert_match(/!!before_perform_foo!! \[nil, "foo", 10\]/, out)
330
+ assert_match(/before_perform_foo.*Completed/m, out)
331
+ refute_match(/Fail ran!!/, out)
332
+ refute_match(/HookFailError/, out)
333
+ end # stopping perform
334
+
335
+ it "should use the connection given as an argument" do
336
+ worker = @worker_class.new('foo.bar')
337
+ connection = mock('connection')
338
+ worker.expects(:reserve_job).with(connection).returns(stub_everything('job'))
339
+ capture_stdout { worker.work_one_job(connection) }
340
+ end
341
+
342
+ after do
343
+ Backburner.configure do |config|
344
+ config.max_job_retries = 0
345
+ config.retry_delay = 5
346
+ config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) }
347
+ end
348
+ end
349
+ end # work_one_job
350
+ end # Worker
@@ -0,0 +1,104 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+ require File.expand_path('../../fixtures/test_jobs', __FILE__)
3
+ require File.expand_path('../../fixtures/hooked', __FILE__)
4
+
5
+ describe "Backburner::Workers::Threading worker" do
6
+ before do
7
+ Backburner.default_queues.clear
8
+ @worker_class = Backburner::Workers::Threading
9
+ @worker_class.shutdown_timeout = 2
10
+ end
11
+
12
+ describe "for prepare method" do
13
+ it "should make tube names array always unique to avoid duplication" do
14
+ worker = @worker_class.new(["foo", "demo.test.foo"])
15
+ capture_stdout { worker.prepare }
16
+ assert_equal ["demo.test.foo"], worker.tube_names
17
+ end
18
+
19
+ it 'creates a thread pool per queue' do
20
+ worker = @worker_class.new(%w(foo bar))
21
+ capture_stdout { worker.prepare }
22
+ assert_equal 2, worker.instance_variable_get("@thread_pools").keys.size
23
+ end
24
+
25
+ it 'uses Concurrent.processor_count if no custom thread count is provided' do
26
+ worker = @worker_class.new("foo")
27
+ capture_stdout { worker.prepare }
28
+ assert_equal ::Concurrent.processor_count, worker.instance_variable_get("@thread_pools")["demo.test.foo"].max_length
29
+ end
30
+ end # prepare
31
+
32
+ describe "for process_tube_names method" do
33
+ it "should interpret the job_name:threads_limit format" do
34
+ worker = @worker_class.new(["foo:4"])
35
+ assert_equal ["foo"], worker.tube_names
36
+ end
37
+
38
+ it "should interpret correctly even if missing values" do
39
+ tubes = %W(foo1:2 foo2)
40
+ worker = @worker_class.new(tubes)
41
+ assert_equal %W(foo1 foo2), worker.tube_names
42
+ end
43
+
44
+ it "should store interpreted values correctly" do
45
+ tubes = %W(foo1 foo2:2)
46
+ worker = @worker_class.new(tubes)
47
+ assert_equal({
48
+ "demo.test.foo1" => { :threads => nil },
49
+ "demo.test.foo2" => { :threads => 2 }
50
+ }, worker.instance_variable_get("@tubes_data"))
51
+ end
52
+ end # process_tube_names
53
+
54
+ describe 'working a queue' do
55
+ before do
56
+ @worker = @worker_class.new(["foo:3"])
57
+ capture_stdout { @worker.prepare }
58
+ $worker_test_count = 0
59
+ $worker_success = false
60
+ end
61
+
62
+ it 'runs work_on_job per thread' do
63
+ clear_jobs!("foo")
64
+ job_count=10
65
+ # TestJob adds the given arguments together and then to $worker_test_count
66
+ job_count.times { @worker_class.enqueue TestJob, [1, 0], :queue => "foo" }
67
+ capture_stdout do
68
+ @worker.start(false) # don't wait for shutdown
69
+ sleep 0.5 # Wait for threads to do their work
70
+ end
71
+ assert_equal job_count, $worker_test_count
72
+ end
73
+ end # working a queue
74
+
75
+ describe 'shutting down' do
76
+ before do
77
+ @thread_count = 3
78
+ @worker = @worker_class.new(["threaded-shutdown:#{@thread_count}"])
79
+ @worker.exit_on_shutdown = false
80
+ $worker_test_count = 0
81
+ clear_jobs!("threaded-shutdown")
82
+ end
83
+
84
+ it 'gracefully exits and completes all in-flight jobs' do
85
+ 6.times { @worker_class.enqueue TestSlowJob, [1, 0], :queue => "threaded-shutdown" }
86
+ Thread.new { sleep 0.1; @worker.self_write.puts("TERM") }
87
+ capture_stdout do
88
+ @worker.start
89
+ end
90
+
91
+ assert_equal @thread_count, $worker_test_count
92
+ end
93
+
94
+ it 'forces an exit when a job is stuck' do
95
+ 6.times { @worker_class.enqueue TestStuckJob, [1, 0], :queue => "threaded-shutdown" }
96
+ Thread.new { sleep 0.1; @worker.self_write.puts("TERM") }
97
+ capture_stdout do
98
+ @worker.start
99
+ end
100
+
101
+ assert_equal 0, $worker_test_count
102
+ end
103
+ end
104
+ end