backburner 1.7.0 → 1.8.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +45 -0
  3. data/.github/workflows/github-release.yml +58 -0
  4. data/.github/workflows/release.yml +221 -0
  5. data/CHANGELOG.md +29 -0
  6. data/Gemfile +4 -1
  7. data/HOOKS.md +23 -14
  8. data/README.md +32 -9
  9. data/backburner.gemspec +3 -0
  10. data/bin/backburner +1 -1
  11. data/lib/active_job/queue_adapters/backburner_adapter.rb +10 -3
  12. data/lib/backburner/configuration.rb +7 -0
  13. data/lib/backburner/connection.rb +4 -2
  14. data/lib/backburner/enqueue_pool.rb +88 -0
  15. data/lib/backburner/helpers.rb +77 -22
  16. data/lib/backburner/job.rb +46 -7
  17. data/lib/backburner/logger.rb +9 -0
  18. data/lib/backburner/performable.rb +1 -1
  19. data/lib/backburner/queue.rb +37 -19
  20. data/lib/backburner/version.rb +1 -1
  21. data/lib/backburner/worker.rb +112 -21
  22. data/lib/backburner/workers/forking.rb +4 -2
  23. data/lib/backburner/workers/simple.rb +5 -0
  24. data/lib/backburner/workers/threading.rb +1 -7
  25. data/lib/backburner/workers/threads_on_fork.rb +58 -23
  26. data/lib/backburner.rb +7 -1
  27. data/test/active_job_adapter_test.rb +24 -0
  28. data/test/back_burner_test.rb +13 -0
  29. data/test/connection_test.rb +22 -0
  30. data/test/fixtures/active_job_app.rb +53 -0
  31. data/test/fixtures/autoload/test_broken_name_job.rb +4 -0
  32. data/test/fixtures/autoload/test_broken_no_method_job.rb +4 -0
  33. data/test/fixtures/autoload/test_broken_require_job.rb +4 -0
  34. data/test/fixtures/autoload/test_broken_runtime_job.rb +4 -0
  35. data/test/fixtures/autoload/test_broken_syntax_job.rb +4 -0
  36. data/test/fixtures/autoload/test_broken_timeout_job.rb +4 -0
  37. data/test/fixtures/autoload/test_same_name_job.rb +5 -0
  38. data/test/fixtures/test_fork_jobs.rb +5 -0
  39. data/test/fixtures/test_jobs.rb +53 -0
  40. data/test/fixtures/test_queue_settings.rb +41 -1
  41. data/test/helpers/templogger.rb +1 -1
  42. data/test/helpers_test.rb +184 -0
  43. data/test/job_test.rb +22 -0
  44. data/test/logger_test.rb +27 -0
  45. data/test/performable_test.rb +50 -0
  46. data/test/queue_test.rb +63 -0
  47. data/test/test_helper.rb +57 -0
  48. data/test/worker_test.rb +346 -1
  49. data/test/workers/forking_worker_test.rb +19 -1
  50. data/test/workers/simple_worker_test.rb +272 -0
  51. data/test/workers/threading_worker_test.rb +22 -1
  52. data/test/workers/threads_on_fork_worker_test.rb +279 -12
  53. metadata +54 -6
  54. data/.travis.yml +0 -29
  55. data/circle.yml +0 -3
@@ -11,4 +11,44 @@ class TestJobSettingsOverride
11
11
  queue_garbage_limit 1000
12
12
  queue_retry_limit 2
13
13
  def self.perform; end
14
- end
14
+ end
15
+ class TestTubeLimitsJob
16
+ include Backburner::Queue
17
+ queue "tube-limits"
18
+ queue_jobs_limit 5
19
+ queue_garbage_limit 100
20
+ queue_retry_limit 3
21
+ def self.perform; end
22
+ end
23
+
24
+ # Shares its parent's tube, whose options stay the parent's
25
+ class TestTubeLimitsChildJob < TestTubeLimitsJob
26
+ queue_jobs_limit 1
27
+ queue_garbage_limit 7
28
+ end
29
+
30
+ class TestTubeLimitsLambdaJob
31
+ include Backburner::Queue
32
+ queue lambda { |klass| "tube-limits-#{klass.name.downcase}" }
33
+ queue_jobs_limit 3
34
+ def self.perform; end
35
+ end
36
+
37
+ # Gets a tube of its own from the inherited lambda, so its options are its own
38
+ class TestTubeLimitsLambdaChildJob < TestTubeLimitsLambdaJob
39
+ queue_jobs_limit 4
40
+ end
41
+
42
+ class TestTubeFewLimitsJob
43
+ include Backburner::Queue
44
+ queue "tube-few-limits"
45
+ queue_retry_limit 2
46
+ def self.perform; end
47
+ end
48
+
49
+ # Shares its parent's tube and fills in the limits the parent leaves unset
50
+ class TestTubeFewLimitsChildJob < TestTubeFewLimitsJob
51
+ queue_jobs_limit 3
52
+ queue_garbage_limit 50
53
+ queue_retry_limit 9
54
+ end
@@ -17,6 +17,6 @@ class Templogger
17
17
  end
18
18
 
19
19
  def close
20
- @file.close
20
+ @file.close! # unlink it now: a Tempfile finalizer left for GC calls Process.pid, which some tests mock
21
21
  end
22
22
  end
data/test/helpers_test.rb CHANGED
@@ -42,6 +42,19 @@ describe "Backburner::Helpers module" do
42
42
  output = exception_message(RuntimeError.new("test"))
43
43
  assert_match(/Exception RuntimeError/, output)
44
44
  end
45
+
46
+ it "shortens the backtrace paths under a working directory that has regexp characters in its name" do
47
+ ["jobs[1]", "jobs["].each do |name|
48
+ Dir.mktmpdir do |root|
49
+ dir = File.join(File.realpath(root), name)
50
+ Dir.mkdir(dir)
51
+ error = RuntimeError.new("test")
52
+ error.set_backtrace(["#{dir}/app/jobs/report.rb:3:in `perform'"])
53
+ output = Dir.chdir(dir) { exception_message(error) }
54
+ assert_equal "Exception RuntimeError -> test\n app/jobs/report.rb:3:in `perform'", output, name
55
+ end
56
+ end
57
+ end
45
58
  end # exception_message
46
59
 
47
60
  describe "for queue_config" do
@@ -85,6 +98,118 @@ describe "Backburner::Helpers module" do
85
98
  test = lambda { "email/send_news" }
86
99
  assert_equal "test.foo.job.email/send-news", expand_tube_name(test)
87
100
  end #lambdas
101
+
102
+ it "removes prefix if configured as nil" do
103
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => nil, :namespace_separator => '.'))
104
+
105
+ test = stub(:queue => "email/send_news")
106
+ assert_equal "email/send-news", expand_tube_name(test)
107
+ end
108
+
109
+ it "removes prefix if configured as false" do
110
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => false, :namespace_separator => '.'))
111
+
112
+ test = stub(:queue => "email/send_news")
113
+ assert_equal "email/send-news", expand_tube_name(test)
114
+ end
115
+
116
+ # Tubes of 1.7.0 with an empty namespace hold queued jobs: keep their names
117
+ it "starts names with the separator if configured as empty" do
118
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => "", :namespace_separator => '.', :primary_queue => "backburner-jobs"))
119
+
120
+ assert_equal ".email/send-news", expand_tube_name("email/send_news")
121
+ assert_equal ".email/send-news", expand_tube_name(stub(:queue => "email/send_news"))
122
+ assert_equal ".backburner-jobs", expand_tube_name(RuntimeError)
123
+ assert_equal ".mailers", expand_tube_name("mailers")
124
+ end
125
+
126
+ # A tube name is where queued jobs wait, so a namespace that works keeps every name it gives, including
127
+ # the odd ones of queue names that start with the namespace ("appmail" -> "app.mail")
128
+ it "keeps the tube names it gives for namespaces without regexp characters other than dots" do
129
+ queues = lambda do |namespace, separator|
130
+ ["default", "mailers", "production_mailers", "newsletter_sender", "email/send_news", "Admin::ReportJob",
131
+ "#{namespace}mail", "#{namespace}-reports", "#{namespace.tr('.', '-')}-sync", "#{namespace}#{separator}reports"]
132
+ end
133
+ {
134
+ ["backburner.worker.queue", "."] => %w[
135
+ backburner.worker.queue.default backburner.worker.queue.mailers backburner.worker.queue.production-mailers
136
+ backburner.worker.queue.newsletter-sender backburner.worker.queue.email/send-news
137
+ backburner.worker.queue.admin/report-job backburner.worker.queue.mail backburner.worker.queue.-reports
138
+ backburner.worker.queue.-sync backburner.worker.queue.reports backburner.worker.queue.backburner-jobs
139
+ ],
140
+ ["backburner.worker.queue", "_"] => %w[
141
+ backburner.worker.queue_default backburner.worker.queue_mailers backburner.worker.queue_production-mailers
142
+ backburner.worker.queue_newsletter-sender backburner.worker.queue_email/send-news
143
+ backburner.worker.queue_admin/report-job backburner.worker.queue_mail backburner.worker.queue_-reports
144
+ backburner.worker.queue_-sync backburner.worker.queue_-reports backburner.worker.queue_backburner-jobs
145
+ ],
146
+ ["backburner", "."] => %w[
147
+ backburner.default backburner.mailers backburner.production-mailers backburner.newsletter-sender
148
+ backburner.email/send-news backburner.admin/report-job backburner.mail backburner.-reports backburner.-sync
149
+ backburner.reports backburner.-jobs
150
+ ],
151
+ ["backburner", "_"] => %w[
152
+ backburner_default backburner_mailers backburner_production-mailers backburner_newsletter-sender
153
+ backburner_email/send-news backburner_admin/report-job backburner_mail backburner_-reports backburner_-sync
154
+ backburner_-reports backburner_-jobs
155
+ ],
156
+ ["app", "."] => %w[
157
+ app.default app.mailers app.production-mailers app.newsletter-sender app.email/send-news app.admin/report-job
158
+ app.mail app.-reports app.-sync app.reports app.backburner-jobs
159
+ ],
160
+ ["app", "_"] => %w[
161
+ app_default app_mailers app_production-mailers app_newsletter-sender app_email/send-news app_admin/report-job
162
+ app_mail app_-reports app_-sync app_-reports app_backburner-jobs
163
+ ],
164
+ ["app.prod", "."] => %w[
165
+ app.prod.default app.prod.mailers app.prod.production-mailers app.prod.newsletter-sender
166
+ app.prod.email/send-news app.prod.admin/report-job app.prod.mail app.prod.-reports app.prod.-sync
167
+ app.prod.reports app.prod.backburner-jobs
168
+ ],
169
+ ["app.prod", "_"] => %w[
170
+ app.prod_default app.prod_mailers app.prod_production-mailers app.prod_newsletter-sender
171
+ app.prod_email/send-news app.prod_admin/report-job app.prod_mail app.prod_-reports app.prod_-sync
172
+ app.prod_-reports app.prod_backburner-jobs
173
+ ],
174
+ ["my-app", "."] => %w[
175
+ my-app.default my-app.mailers my-app.production-mailers my-app.newsletter-sender my-app.email/send-news
176
+ my-app.admin/report-job my-app.mail my-app.-reports my-app.-sync my-app.reports my-app.backburner-jobs
177
+ ],
178
+ ["my-app", "_"] => %w[
179
+ my-app_default my-app_mailers my-app_production-mailers my-app_newsletter-sender my-app_email/send-news
180
+ my-app_admin/report-job my-app_mail my-app_-reports my-app_-sync my-app_-reports my-app_backburner-jobs
181
+ ],
182
+ ["MyApp", "."] => %w[
183
+ MyApp.default MyApp.mailers MyApp.production-mailers MyApp.newsletter-sender MyApp.email/send-news
184
+ MyApp.admin/report-job MyApp.my-appmail MyApp.my-app-reports MyApp.my-app-sync MyApp.my-app.reports
185
+ MyApp.backburner-jobs
186
+ ],
187
+ ["MyApp", "_"] => %w[
188
+ MyApp_default MyApp_mailers MyApp_production-mailers MyApp_newsletter-sender MyApp_email/send-news
189
+ MyApp_admin/report-job MyApp_my-appmail MyApp_my-app-reports MyApp_my-app-sync MyApp_my-app-reports
190
+ MyApp_backburner-jobs
191
+ ]
192
+ }.each do |(namespace, separator), tube_names|
193
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => namespace, :namespace_separator => separator, :primary_queue => "backburner-jobs"))
194
+ assert_equal tube_names, queues.call(namespace, separator).map { |queue| expand_tube_name(queue) } + [expand_tube_name(RuntimeError)],
195
+ [namespace, separator].inspect
196
+ end
197
+ end
198
+
199
+ it "matches a namespace that isn't a valid regexp literally" do
200
+ { "app(" => ["app(.default", "app(.mail"], "jobs\\" => ["jobs\\.default", "jobs\\.mail"] }.each do |namespace, tube_names|
201
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => namespace, :namespace_separator => ".", :primary_queue => "backburner-jobs"))
202
+ assert_equal tube_names, [expand_tube_name("default"), expand_tube_name("#{namespace}mail")], namespace
203
+ end
204
+ end
205
+
206
+ it "uses the bare primary queue without a prefix" do
207
+ [nil, false].each do |namespace|
208
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => namespace, :namespace_separator => '-', :primary_queue => "backburner-jobs"))
209
+ assert_equal "backburner-jobs", expand_tube_name(RuntimeError), namespace.inspect
210
+ assert_equal "queue-name", expand_tube_name("queue_name"), namespace.inspect
211
+ end
212
+ end
88
213
  end # expand_tube_name
89
214
 
90
215
  describe "for alternative namespace separator" do
@@ -93,6 +218,17 @@ describe "Backburner::Helpers module" do
93
218
  it "uses alternative namespace separator" do
94
219
  assert_equal "test-queue-name", expand_tube_name("queue_name")
95
220
  end # simple string
221
+
222
+ it "joins the namespace and queue name without a separator if configured as empty" do
223
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => "test", :namespace_separator => '', :primary_queue => "backburner-jobs"))
224
+ assert_equal "testqueue-name", expand_tube_name("queue_name")
225
+ assert_equal "testbackburner-jobs", expand_tube_name(RuntimeError)
226
+ end
227
+
228
+ it "keeps a trailing dot in the namespace when the separator is empty" do
229
+ Backburner.stubs(:configuration).returns(stub(:tube_namespace => "test.", :namespace_separator => '', :primary_queue => "backburner-jobs"))
230
+ assert_equal "test.queue-name", expand_tube_name("queue_name")
231
+ end
96
232
  end
97
233
 
98
234
  describe "for resolve_priority method" do
@@ -147,6 +283,31 @@ describe "Backburner::Helpers module" do
147
283
  it "supports default pri for null values" do
148
284
  assert_equal 1000, resolve_priority(nil)
149
285
  end
286
+
287
+ it "supports a priority label as the default" do
288
+ Backburner.configure { |config| config.default_priority = :high }
289
+ assert_equal 0, resolve_priority(nil)
290
+ assert_equal 0, resolve_priority(stub(:queue_priority => nil))
291
+ assert_equal 0, resolve_priority(:unknown_label)
292
+ assert_equal 200, resolve_priority(:low)
293
+ end
294
+
295
+ it "supports a custom or aliased priority label as the default" do
296
+ Backburner.configure { |config| config.priority_labels = { :foo => 5, :bar => 'foo' }; config.default_priority = 'bar' }
297
+ assert_equal 5, resolve_priority(nil)
298
+ end
299
+
300
+ it "leaves an unknown default label as configured" do
301
+ Backburner.configure { |config| config.default_priority = :unknown_label }
302
+ assert_equal :unknown_label, resolve_priority(nil)
303
+ end
304
+
305
+ it "stops at a cycle of priority labels" do
306
+ Backburner.configure { |config| config.priority_labels = { :a => :b, :b => 'a' }; config.default_priority = :a }
307
+ assert_equal :a, resolve_priority(nil), "a default label cycle is left as configured"
308
+ Backburner.configure { |config| config.default_priority = 1000 }
309
+ assert_equal 1000, resolve_priority(:b), "a job label cycle falls back to the default"
310
+ end
150
311
  end # resolve_priority
151
312
 
152
313
  describe "for resolve_respond_timeout method" do
@@ -178,6 +339,19 @@ describe "Backburner::Helpers module" do
178
339
  it "supports default ttr for null values" do
179
340
  assert_equal 300, resolve_respond_timeout(nil)
180
341
  end
342
+
343
+ it "rounds a fractional respond_timeout up to whole seconds, from a value, a class or the default" do
344
+ assert_equal 1, resolve_respond_timeout(0.2)
345
+ assert_equal 2, resolve_respond_timeout(stub(:queue_respond_timeout => 1.5))
346
+ Backburner.configure { |config| config.respond_timeout = 2.5 }
347
+ assert_equal 3, resolve_respond_timeout(nil)
348
+ end
349
+
350
+ it "keeps an Integer as it is and ignores a value that isn't finite, as before" do
351
+ assert_equal(-5, resolve_respond_timeout(-5))
352
+ assert_equal 300, resolve_respond_timeout(Float::INFINITY)
353
+ assert_equal 300, resolve_respond_timeout(stub(:queue_respond_timeout => Float::NAN))
354
+ end
181
355
  end # resolve_respond_timeout
182
356
 
183
357
  describe "for resolve_max_job_retries method" do
@@ -218,6 +392,11 @@ describe "Backburner::Helpers module" do
218
392
  end
219
393
  after { Backburner.configure { |config| config.retry_delay = @original_retry_delay } }
220
394
 
395
+ it "ignores a retry_delay that isn't finite, as before" do
396
+ assert_equal 300, resolve_retry_delay(stub(:queue_retry_delay => Float::NAN))
397
+ assert_equal 300, resolve_retry_delay(Float::INFINITY)
398
+ end
399
+
221
400
  it "supports fix num retry_delay" do
222
401
  assert_equal 500, resolve_retry_delay(500)
223
402
  end
@@ -240,6 +419,11 @@ describe "Backburner::Helpers module" do
240
419
  it "supports default retry_delay for null values" do
241
420
  assert_equal 300, resolve_retry_delay(nil)
242
421
  end
422
+
423
+ it "supports a fractional retry_delay from a value or a class" do
424
+ assert_equal 2.5, resolve_retry_delay(2.5)
425
+ assert_equal 1.5, resolve_retry_delay(stub(:queue_retry_delay => 1.5))
426
+ end
243
427
  end # resolve_retry_delay
244
428
 
245
429
  describe "for resolve_retry_delay_proc method" do
data/test/job_test.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../test_helper', __FILE__)
2
+ require File.expand_path('../fixtures/test_jobs', __FILE__)
2
3
 
3
4
  module NestedDemo
4
5
  class TestJobC
@@ -110,6 +111,27 @@ describe "Backburner::Job module" do
110
111
  assert_raises(Backburner::Job::JobNotFound) { @job.process }
111
112
  end # error class
112
113
  end # invalid
114
+
115
+ describe "with a class an autoloader can't load" do
116
+ before do
117
+ @task_body = { "class" => "AutoloadingDemo::TestJobGone", "args" => [56] }
118
+ @task = stub(:body => @task_body, :ttr => 120, :delete => true, :bury => true)
119
+ @task.expects(:delete).never
120
+ end
121
+
122
+ it "should raise JobNotFound" do
123
+ @job = Backburner::Job.new(@task)
124
+ assert_raises(Backburner::Job::JobNotFound) { @job.process }
125
+ end
126
+ end # autoloader
127
+
128
+ describe "with a class whose file fails to load" do
129
+ it "should raise JobNotFound naming the cause on one line" do
130
+ task = stub(:body => { "class" => "TestBrokenNameJob", "args" => [56] }, :ttr => 120, :delete => true, :bury => true)
131
+ error = assert_raises(Backburner::Job::JobNotFound) { Backburner::Job.new(task).process }
132
+ assert_match(/\ATestBrokenNameJob \(NameError: uninitialized constant (TestBrokenNameJob::)?TestMissingConcern\)\z/, error.message)
133
+ end
134
+ end # broken class file
113
135
  end # process
114
136
 
115
137
  describe "for simple delegation method" do
data/test/logger_test.rb CHANGED
@@ -41,4 +41,31 @@ describe "Backburner::Logger module" do
41
41
  Backburner.configure { |config| config.logger = nil }
42
42
  end
43
43
  end # log_error
44
+
45
+ describe "for log_warn method" do
46
+ it "prints out to std err" do
47
+ output = capture_stdout { log_warn("baz") }
48
+ assert_equal "baz\n", output
49
+ end
50
+
51
+ it "can be configured to log to logger" do
52
+ Backburner.configure { |config| config.logger = @logger }
53
+ log_warn("baz")
54
+ assert_match(/W,.*?baz/, @strio.string)
55
+ end
56
+
57
+ it "logs as an error to a logger without a public warn method" do
58
+ logged = []
59
+ logger = Object.new # the README asks a logger for info and error only
60
+ logger.define_singleton_method(:info) { |msg| logged << [:info, msg] }
61
+ logger.define_singleton_method(:error) { |msg| logged << [:error, msg] }
62
+ Backburner.configure { |config| config.logger = logger }
63
+ log_warn("baz")
64
+ assert_equal [[:error, "baz"]], logged
65
+ end
66
+
67
+ after do
68
+ Backburner.configure { |config| config.logger = nil }
69
+ end
70
+ end # log_warn
44
71
  end
@@ -22,6 +22,24 @@ end
22
22
  class AsyncInstanceMethodsTestObj < PerformableTestObj; end
23
23
  class AsyncStaticMethodsTestObj < PerformableTestObj; end
24
24
 
25
+ class AsyncSettingsTestObj < TestObj
26
+ include Backburner::Performable
27
+ queue "performable-settings"
28
+ queue_priority 10
29
+ queue_respond_timeout 300
30
+ queue_max_job_retries 3
31
+ handle_asynchronously :foo
32
+ handle_static_asynchronously :bar
33
+ end
34
+
35
+ class QueueThenPerformableTestObj < TestObj
36
+ include Backburner::Queue
37
+ queue "queue-then-performable"
38
+ queue_priority 20
39
+ queue_respond_timeout 200
40
+ include Backburner::Performable
41
+ end
42
+
25
43
 
26
44
 
27
45
  describe "Backburner::Performable module" do
@@ -84,5 +102,37 @@ describe "Backburner::Performable module" do
84
102
  Backburner::Worker.expects(:enqueue).with(AsyncStaticMethodsTestObj, [nil, :bar_without_async, true, false], has_entries(:pri => 5000, :queue => "garply"))
85
103
  AsyncStaticMethodsTestObj.bar(true, false)
86
104
  end
105
+
106
+ describe "on a class with queue settings" do
107
+ before { clear_jobs!("performable-settings") }
108
+ after { clear_jobs!("performable-settings") }
109
+
110
+ it "keeps the class's queue settings" do
111
+ assert_equal "performable-settings", AsyncSettingsTestObj.queue
112
+ assert_equal 10, AsyncSettingsTestObj.queue_priority
113
+ assert_equal 300, AsyncSettingsTestObj.queue_respond_timeout
114
+ assert_equal 3, AsyncSettingsTestObj.queue_max_job_retries
115
+ end
116
+
117
+ it "enqueues instance and class method calls with the class's queue settings" do
118
+ AsyncSettingsTestObj.new.foo(true, false)
119
+ AsyncSettingsTestObj.bar(false, true)
120
+ [[56, "foo_without_async", true, false], [nil, "bar_without_async", false, true]].each do |args|
121
+ pop_one_job("performable-settings") do |job, body|
122
+ assert_equal ["AsyncSettingsTestObj", args], [body["class"], body["args"]]
123
+ assert_equal [10, 300], [job.pri, job.ttr]
124
+ job.delete
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end # handle_asynchronously
130
+
131
+ describe "when included after Backburner::Queue" do
132
+ it "keeps the queue settings the class already set" do
133
+ assert_equal "queue-then-performable", QueueThenPerformableTestObj.queue
134
+ assert_equal 20, QueueThenPerformableTestObj.queue_priority
135
+ assert_equal 200, QueueThenPerformableTestObj.queue_respond_timeout
136
+ end
87
137
  end
88
138
  end
data/test/queue_test.rb CHANGED
@@ -5,6 +5,33 @@ module NestedDemo
5
5
  class TestJobB; include Backburner::Queue; end
6
6
  end
7
7
 
8
+ module InheritDemo
9
+ class ParentJob
10
+ include Backburner::Queue
11
+ queue "inherit-parent"
12
+ queue_priority 10
13
+ queue_respond_timeout 77
14
+ queue_max_job_retries 4
15
+ queue_retry_delay 9
16
+ queue_retry_delay_proc lambda { |min_retry_delay, num_retries| 42 }
17
+ queue_jobs_limit 3
18
+ queue_garbage_limit 50
19
+ queue_retry_limit 6
20
+ end
21
+ class ChildJob < ParentJob; end
22
+ class GrandchildJob < ChildJob; end
23
+ class OverrideJob < ParentJob
24
+ queue "inherit-override"
25
+ queue_priority 20
26
+ end
27
+ class ReincludedJob < ParentJob; include Backburner::Queue; end
28
+ class LambdaParentJob
29
+ include Backburner::Queue
30
+ queue lambda { |klass| klass.name }
31
+ end
32
+ class LambdaChildJob < LambdaParentJob; end
33
+ end
34
+
8
35
  describe "Backburner::Queue module" do
9
36
  describe "contains known_queue_classes" do
10
37
  it "has all defined known queues" do
@@ -66,4 +93,40 @@ describe "Backburner::Queue module" do
66
93
  assert_equal retry_delay_proc.call(2, 1), NestedDemo::TestJobB.queue_retry_delay_proc.call(2, 1)
67
94
  end
68
95
  end # queue_retry_delay_proc
96
+
97
+ describe "for subclasses" do
98
+ settings = [:queue, :queue_priority, :queue_respond_timeout, :queue_max_job_retries, :queue_retry_delay,
99
+ :queue_retry_delay_proc, :queue_jobs_limit, :queue_garbage_limit, :queue_retry_limit]
100
+
101
+ it "inherits every queue setting from the superclass" do
102
+ settings.each do |setting|
103
+ refute_nil InheritDemo::ParentJob.send(setting), setting
104
+ assert_equal InheritDemo::ParentJob.send(setting), InheritDemo::ChildJob.send(setting), setting
105
+ assert_equal InheritDemo::ParentJob.send(setting), InheritDemo::GrandchildJob.send(setting), setting
106
+ end
107
+ end
108
+
109
+ it "uses its own settings over inherited ones without changing the superclass" do
110
+ assert_equal "inherit-override", InheritDemo::OverrideJob.queue
111
+ assert_equal 20, InheritDemo::OverrideJob.queue_priority
112
+ assert_equal 77, InheritDemo::OverrideJob.queue_respond_timeout
113
+ assert_equal "inherit-parent", InheritDemo::ParentJob.queue
114
+ assert_equal 10, InheritDemo::ParentJob.queue_priority
115
+ end
116
+
117
+ it "inherits the superclass settings when it includes Backburner::Queue again" do
118
+ assert_equal "inherit-parent", InheritDemo::ReincludedJob.queue
119
+ assert_equal 10, InheritDemo::ReincludedJob.queue_priority
120
+ end
121
+
122
+ it "calls an inherited queue lambda with the subclass" do
123
+ assert_equal "InheritDemo::LambdaChildJob", InheritDemo::LambdaChildJob.queue
124
+ end
125
+
126
+ it "registers subclasses as known queue classes, once each" do
127
+ [InheritDemo::ChildJob, InheritDemo::GrandchildJob, InheritDemo::OverrideJob, InheritDemo::ReincludedJob].each do |klass|
128
+ assert_equal 1, Backburner::Worker.known_queue_classes.count(klass), klass.name
129
+ end
130
+ end
131
+ end # subclasses
69
132
  end # Backburner::Queue
data/test/test_helper.rb CHANGED
@@ -110,6 +110,34 @@ class MiniTest::Spec
110
110
  connection.close if connection
111
111
  end
112
112
 
113
+ # A beanstalkd of the test's own (on @port) that it can stop and start, for outage tests:
114
+ # pick_beanstalkd_port in `before`, then start_beanstalkd, and stop_beanstalkd in `after`
115
+ def pick_beanstalkd_port
116
+ @beanstalkd_bin = ENV['BEANSTALKD_BIN'] || ENV['PATH'].split(File::PATH_SEPARATOR).
117
+ map { |dir| File.join(dir, 'beanstalkd') }.find { |file| File.executable?(file) }
118
+ skip "needs a beanstalkd binary on PATH or in BEANSTALKD_BIN" unless @beanstalkd_bin
119
+ server = TCPServer.new('127.0.0.1', 0)
120
+ @port = server.addr[1]
121
+ server.close
122
+ end
123
+
124
+ def start_beanstalkd
125
+ @beanstalkd_pid = Process.spawn(@beanstalkd_bin, '-l', '127.0.0.1', '-p', @port.to_s, [:out, :err] => File::NULL)
126
+ wait_for { (TCPSocket.new('127.0.0.1', @port).close; true) rescue false }
127
+ end
128
+
129
+ def stop_beanstalkd
130
+ return unless @beanstalkd_pid
131
+ Process.kill(:KILL, @beanstalkd_pid)
132
+ Process.wait(@beanstalkd_pid)
133
+ @beanstalkd_pid = nil
134
+ end
135
+
136
+ def wait_for(seconds = 15)
137
+ deadline = Time.now + seconds
138
+ sleep 0.1 until yield || Time.now > deadline
139
+ end
140
+
113
141
  # Simulates a broken connection for any Beaneater::Connection. Will
114
142
  # simulate a restored connection after `reconnects_after`. This is expected
115
143
  # to be used when ensuring a Beaneater connection is open, therefore
@@ -122,4 +150,33 @@ class MiniTest::Spec
122
150
  end
123
151
  TCPSocket.expects(:new).times(returns.size).returns(*returns)
124
152
  end
153
+
154
+ # Whether a forked child's GC was disabled by the time the interpreter tore it down (when its
155
+ # at_exit handlers run). Yields the child's body, which ends as `how` says (:return, :raise, or
156
+ # :shutdown, which calls Kernel.exit as the TERM and INT traps do); the block forks a child that
157
+ # runs it and waits for the child. Fails if the GC is already off here (the child would inherit
158
+ # that), or if the child never ran its at_exit handlers (it left by exit!, or crashed).
159
+ def gc_disabled_at_child_exit?(worker, how)
160
+ gc_was_disabled = GC.enable # returns whether it was disabled
161
+ GC.disable if gc_was_disabled
162
+ refute gc_was_disabled, "the GC was already disabled in the test process"
163
+ reader, writer = IO.pipe
164
+ yield(lambda do
165
+ reader.close
166
+ $stdout.reopen(File::NULL)
167
+ $stderr.reopen(File::NULL)
168
+ at_exit { writer.write(GC.disable ? "disabled" : "enabled"); writer.close }
169
+ case how
170
+ when :raise then raise "the child failed"
171
+ when :shutdown then worker.shutdown
172
+ end
173
+ end)
174
+ writer.close
175
+ state = reader.read
176
+ flunk "the child never ran its at_exit handlers (it left by exit!, or crashed)" if state.empty?
177
+ state == "disabled"
178
+ ensure
179
+ reader.close if reader && !reader.closed?
180
+ writer.close if writer && !writer.closed?
181
+ end
125
182
  end # MiniTest::Spec