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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -4
- data/README.md +70 -14
- data/lib/active_job/queue_adapters/backburner_adapter.rb +9 -2
- data/lib/backburner/cli.rb +1 -1
- data/lib/backburner/enqueue_pool.rb +2 -0
- data/lib/backburner/performable.rb +28 -3
- data/lib/backburner/version.rb +1 -1
- data/lib/backburner/worker.rb +38 -4
- data/lib/backburner/workers/threading.rb +16 -0
- data/lib/backburner/workers/threads_on_fork.rb +1 -0
- metadata +8 -82
- data/.github/workflows/ci.yml +0 -45
- data/.github/workflows/github-release.yml +0 -58
- data/.github/workflows/release.yml +0 -221
- data/.gitignore +0 -17
- data/CONTRIBUTING.md +0 -37
- data/Gemfile +0 -7
- data/Rakefile +0 -17
- data/TODO +0 -4
- data/backburner.gemspec +0 -31
- data/examples/custom.rb +0 -25
- data/examples/demo.rb +0 -60
- data/examples/god.rb +0 -46
- data/examples/hooked.rb +0 -87
- data/examples/retried.rb +0 -31
- data/examples/simple.rb +0 -43
- data/examples/stress.rb +0 -31
- data/test/active_job_adapter_test.rb +0 -24
- data/test/async_proxy_test.rb +0 -36
- data/test/back_burner_test.rb +0 -101
- data/test/connection_test.rb +0 -201
- data/test/fixtures/active_job_app.rb +0 -53
- data/test/fixtures/active_jobs.rb +0 -11
- data/test/fixtures/autoload/test_broken_name_job.rb +0 -4
- data/test/fixtures/autoload/test_broken_no_method_job.rb +0 -4
- data/test/fixtures/autoload/test_broken_require_job.rb +0 -4
- data/test/fixtures/autoload/test_broken_runtime_job.rb +0 -4
- data/test/fixtures/autoload/test_broken_syntax_job.rb +0 -4
- data/test/fixtures/autoload/test_broken_timeout_job.rb +0 -4
- data/test/fixtures/autoload/test_same_name_job.rb +0 -5
- data/test/fixtures/hooked.rb +0 -122
- data/test/fixtures/test_fork_jobs.rb +0 -77
- data/test/fixtures/test_forking_jobs.rb +0 -56
- data/test/fixtures/test_jobs.rb +0 -140
- data/test/fixtures/test_queue_settings.rb +0 -54
- data/test/helpers/templogger.rb +0 -22
- data/test/helpers_test.rb +0 -462
- data/test/hooks_test.rb +0 -112
- data/test/job_test.rb +0 -207
- data/test/logger_test.rb +0 -71
- data/test/performable_test.rb +0 -138
- data/test/queue_test.rb +0 -132
- data/test/test_helper.rb +0 -182
- data/test/worker_test.rb +0 -502
- data/test/workers/forking_worker_test.rb +0 -199
- data/test/workers/simple_worker_test.rb +0 -622
- data/test/workers/threading_worker_test.rb +0 -125
- 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
|