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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50e2f0b2644659644c0789a09a93c786e1bf7ab914bdadc40e80be8cdf941fb7
4
- data.tar.gz: e86d98085d8fbeb91079d9d52095d2ba62f9c4ec114e28053c8d457398003b35
3
+ metadata.gz: ad41e83594bda50d1b504a4e0e5777c477e5586dc88faddbbad476a81afb34d4
4
+ data.tar.gz: f53ac6873638f40f627bbcd1326f50fbd642e5454bce2c083b809cdc6904863b
5
5
  SHA512:
6
- metadata.gz: f707e632bc937197959a4e919b8f12b010a201b7e18add316c3cb66bf1598219a1089c480f7012f9d888debc63fc1d0c4eb0be85b4a943ac3220dce2453c22e6
7
- data.tar.gz: c79112e436fd4bff2b68cc417de9fe23b782b433882697bae60b937f8c36ae76177e056a3a86cff1ab714da48a59d5a548e8113469382a387967295dc697aee1
6
+ metadata.gz: '0384db95f53944823490c2105948153c8aac2d0fb5e8268c8c6f82895ae722bf293bb93f7a0536967a9ad91a138d37f02d92f4f2b0e6860a749cb830065dd668'
7
+ data.tar.gz: 1e6cbde19cd41a7540346d5d66af0537af1fc53baec32466790217b0643b610cbd7d17f8ea74b1ed6aedd55a26c152d798df86f4ef6b21ac11b5bc848db9d175
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Version 1.9.0 (Nov 1 2026)
4
+
5
+ * The gem now ships only `lib/`, `bin/`, `README.md`, `HOOKS.md`, `CHANGELOG.md` and `LICENSE` (no tests, examples or CI files), and its rubygems.org page links the source, the changelog and the issue tracker. **Behaviour change for the gem's owners:** pushing a release to rubygems.org now requires MFA (`rubygems_mfa_required`); the release workflow's trusted publishing is unaffected. **What to do:** owners who push by hand need MFA enabled on their rubygems.org account
6
+ * Docs: the README's Simple Async Jobs example says a `Backburner::Performable` class's queue defaults to the primary queue (`backburner-jobs`), not one named after the class, and its Persistence examples show the JSON jobs are stored as (`null`, double quotes) on the class's queue
7
+ * Docs: the README says Backburner uses only the first of several `beanstalk_url`s (no client-side federation), and that the Threading worker's `queue:threads` CLI format needs `config.default_worker = Backburner::Workers::Threading`; `examples/god.rb` sets `QUEUE` (which the rake tasks read) instead of `QUEUES`; `examples/custom.rb` enqueues on the queue it works; CONTRIBUTING says the tests need beanstalkd on 127.0.0.1:11300; `backburner --help` shows `-q, --queues QUEUES`; dead links point to beanstalkd.github.io, rubygems.org and rubydoc.info; the 1.5.0 and 1.6.0 changelog entries are filled in from their commits, with their release dates
8
+ * Docs: the README has an Active Job section: setting the adapter, how queues map to tubes, starting workers with `-q` for the Active Job queues (a worker started without `-q` doesn't pick up a queue first used after it started), the TTR, how `retry_on` and `max_job_retries` add up, and using Active Job callbacks rather than Backburner's hooks
9
+ * Active Job: a job's TTR is now its class's `queue_respond_timeout` when it defines one (`def self.queue_respond_timeout; 3600; end`; subclasses inherit it), as for a `Backburner::Queue` job; before, every Active Job job got `config.respond_timeout` (120 seconds by default), so a job that ran longer was stopped with `Backburner::Job::JobTimeout` and retried or buried. Jobs of classes without one still get `config.respond_timeout`. The TTR is set when a job is enqueued, so jobs already queued, or enqueued by an app still on an older backburner, keep the TTR they had. **What to do:** give long-running Active Job classes a `queue_respond_timeout` longer than they take
10
+ * Rails apps: a worker now runs each job inside the app's reloader (`Rails.application.reloader.wrap`), as Rails does for its own Active Job jobs and as Sidekiq does. **Behaviour change:** `ActiveSupport::CurrentAttributes` (a `Current.user` or tenant a job sets) no longer leak into the next job the worker runs; the database connections a job uses go back to the pool when it finishes; Active Record's query cache is on while a job runs, as it is for requests and Active Job jobs; and in development a job runs the app's latest code after a file changes (a reload waits for the jobs running in other threads to finish). On Rails 7.0 and newer, an error a job raises is now also reported to `Rails.error`, as an Active Job job's error already was (including a job whose class this worker doesn't have, each time it's released), but not a `Backburner::Job::RetryJob`, nor the `SystemExit` of a worker stopped by `TERM` or `INT` (or of a job that calls `exit`): an error tracker subscribed to `Rails.error` that `config.on_error` also calls may get it twice. An Active Job job runs in the worker's wrap, so Active Job's own wrap inside it does nothing: it's still wrapped once, but on Rails 7.1 and newer its errors, like other jobs', are now reported with the source `"job.backburner"` instead of `"application.active_support"` (a `Rails.error` subscriber that filters on the source sees the new one). `config.on_error` and the `on_retry` and `on_bury` hooks run after the job's wrap ends, so they no longer see the `CurrentAttributes` the job set, and a database connection they use is taken outside the executor (the `on_failure` hooks still run inside it). Outside Rails nothing changes. **What to do:** nothing, unless a job relies on state an earlier job left in `CurrentAttributes` (pass it as an argument instead), a job waits for a change another process makes to the database (read it inside `ActiveRecord::Base.uncached { ... }`, as the query cache would return the first result again), or an `on_error` handler or retry or bury hook reads the job's `CurrentAttributes` (pass what it needs in the job's arguments, or read it in an `on_failure` hook)
11
+ * Fix "Worker exiting..." often not being logged when a worker stops on `TERM` or `INT`: the process could exit before the thread that logs it ran. The worker now waits up to a second for that line to be written before it exits. **What to do:** nothing
12
+ * `Backburner::Performable` jobs (enqueued with `async`, `handle_asynchronously` or `handle_static_asynchronously`) now call only methods the app defines on the class, its superclasses (such as `ActiveRecord::Base`) or the modules they include, private ones too. **Behaviour change:** a job naming a method the class only gets from Ruby itself (`Object`, `Kernel` and `BasicObject`, or for a class method `Class` and `Module`, including what libraries add to them, such as `to_s`, `frozen?`, `name` or `try`), one of Backburner's own (`queue`, `perform`), or `method_missing` now fails with `NoMethodError`, and is retried and then buried like any other failure. A class that overrides such a method (`def to_s`) still has its own called. **What to do:** if your app enqueues such a method (`User.async.name`), define a method on the class that calls it and enqueue that instead. Jobs already queued keep the method name they were enqueued with, so they fail the same way once the workers are upgraded: to run them, define that same method on the class (`def self.name; super; end`) and kick them from the buried list, or delete them and enqueue the new method
13
+ * Fix the Threading worker taking `shutdown_timeout` (10 seconds by default) to exit on `TERM` or `INT` when it had no jobs to work: its threads waited in `reserve` until the shutdown gave up on them and killed them. Each thread now waits for a job at most a second at a time (or `config.reserve_timeout`, if shorter; a negative one, which made beanstalkd wait forever, counts as a second), so an idle worker exits within about a second. A busy worker is unchanged: its in-flight jobs get up to `shutdown_timeout` to finish before their threads are killed. A job a thread reserves as the shutdown begins is worked like any in-flight job; one still running when its thread is killed is not lost: beanstalkd puts it back in the queue when the worker exits, and it runs again. **What to do:** nothing; an idle Threading worker now sends beanstalkd a `reserve-with-timeout` once a second per thread
14
+
3
15
  ## Version 1.8.0 (Oct 31 2026)
4
16
 
5
17
  * Fix ThreadsOnFork and Forking children crashing on Ruby 3.3 with `[BUG] lazy sweeping underway when freeing object space` when they stopped on a `TERM` or `INT`, on an error, or when a job called `exit`, if a thread in the parent was running finalizers when the child was forked (an interpreter bug fixed in Ruby 3.4). On Ruby 3.3 a forked child now disables the GC as it exits, so its `at_exit` handlers (the app's, and those it inherited from the parent) run with the GC off, and a child running several threads stops its job threads before those handlers run (a job they were working on goes back to the queue and runs again). Other Rubies are unchanged. **What to do:** nothing; on Ruby 3.3 an `at_exit` handler that allocates a lot in a child now grows the child's memory until it exits
@@ -37,13 +49,21 @@
37
49
 
38
50
  * Change `File.exists?`` to `File.exist?` as latest ruby versions have removed support (@ShrutiSaagar).
39
51
 
40
- ## Version 1.6.0 (December 30 2021)
52
+ ## Version 1.6.0 (Dec 30 2021)
41
53
 
42
- * TBD (please help backfill)
54
+ * Allow per-job `queue_retry_delay`, `queue_retry_delay_proc` and `queue_max_job_retries` (@boncey in #162)
55
+ * Fix the beanstalkd links in the README (@a112121788 in #171)
43
56
 
44
- ## Version 1.5.0 (September 10 2018)
57
+ ## Version 1.5.0 (Sep 19 2018)
45
58
 
46
- * TBD
59
+ * Add `Backburner::Job::RetryJob`: raise it from a job to retry it without logging an error (@eltone in #159)
60
+ * Threading worker: shut down gracefully on `TERM` or `INT`, letting running jobs finish; a second signal stops it at once (@eltone in #157)
61
+ * Make the logger's job timings thread-safe (@eltone in #157)
62
+ * Log, retry and bury jobs whose class is unknown instead of raising while handling their failure (@eltone in #154)
63
+ * Fix Ruby warnings about a shadowed and an unused variable (@utilum in #153)
64
+ * Allow concurrent-ruby 1.x after 1.0.1 (`~> 1.0`, `>= 1.0.1`)
65
+ * Docs: fix the link to Adam Wiggins' beanstalkd comparison (@dplummer in #152); add a CI badge (@szTheory in #151)
66
+ * CI: test against Ruby 2.x (@nicolasleger in #150)
47
67
 
48
68
  ## Version 1.4.1 (June 10 2017)
49
69
 
data/README.md CHANGED
@@ -57,7 +57,7 @@ Beanstalkd supports the following features out of the box:
57
57
  | **Fast** | Processes thousands of jobs per second without breaking a sweat. |
58
58
  | **Priorities** | Specify priority so important jobs can be processed quickly. |
59
59
  | **Persistence** | Jobs are stored in memory for speed, but logged to disk for safe keeping. |
60
- | **Federation** | Horizontal scalability provided through federation by the client. |
60
+ | **Federation** | Horizontal scalability provided through federation by the client (Backburner connects to a single beanstalkd: given several `beanstalk_url`s, it uses the first). |
61
61
  | **Error Handling** | Bury any job which causes an error for later debugging and inspection.|
62
62
 
63
63
  Keep in mind that these features are supported out of the box with beanstalk and require no special code within this gem to support.
@@ -70,6 +70,9 @@ Depending on your platform, this should be as simple as (for Ubuntu):
70
70
 
71
71
  $ sudo apt-get install beanstalkd
72
72
 
73
+ beanstalkd has no authentication: anyone who can connect to it can add jobs, so make it reachable only
74
+ from your app's own hosts (bind it to `127.0.0.1` or a private network, or firewall its port).
75
+
73
76
  Add this line to your application's Gemfile:
74
77
 
75
78
  gem 'backburner'
@@ -238,7 +241,7 @@ includes `Backburner::Performable`. Async enqueuing works for both instance and
238
241
  ```ruby
239
242
  class User
240
243
  include Backburner::Performable
241
- queue "user-jobs" # defaults to 'user'
244
+ queue "user-jobs" # defaults to the primary queue, 'backburner-jobs'
242
245
  queue_priority 500 # most urgent priority is 0
243
246
  queue_respond_timeout 300 # number of seconds before job times out (minimum 1; 0 does not disable it)
244
247
 
@@ -263,6 +266,10 @@ This automatically enqueues a job for that user record that will run `activate`
263
266
  Note that you can set the queue name and queue priority at the class level and
264
267
  you are also able to pass `pri`, `ttr`, `delay` and `queue` directly as options into `async`.
265
268
 
269
+ The worker calls only methods your app defines on the class, its superclasses or the modules they include (private
270
+ ones too). A job naming a method every object or class has (`frozen?`, `instance_variable_get`, `name`, ...), or one
271
+ of Backburner's own (`queue`, `perform`), fails with `NoMethodError`.
272
+
266
273
  The queue name used by default is `{namespace}.backburner-jobs` if not otherwise
267
274
  specified.
268
275
 
@@ -372,12 +379,12 @@ example from above. We'll run the following code to create a job:
372
379
  User.async.reset_password(@user.id)
373
380
  ```
374
381
 
375
- The following JSON will be put on the `{namespace}.backburner-jobs` queue:
382
+ The following JSON will be put on the `{namespace}.user-jobs` queue (the class's queue; `{namespace}.backburner-jobs` for a class without one):
376
383
 
377
384
  ``` javascript
378
385
  {
379
- 'class': 'User',
380
- 'args': [nil, 'reset_password', 123]
386
+ "class": "User",
387
+ "args": [null, "reset_password", 123]
381
388
  }
382
389
  ```
383
390
 
@@ -393,8 +400,8 @@ would be stored as:
393
400
 
394
401
  ``` javascript
395
402
  {
396
- 'class': 'User',
397
- 'args': [246, 'activate', 987]
403
+ "class": "User",
404
+ "args": [246, "activate", 987]
398
405
  }
399
406
  ```
400
407
 
@@ -474,7 +481,7 @@ For more information on the threads_on_fork worker, check out the
474
481
 
475
482
  #### More info: Threading Worker (thread-pool-based)
476
483
 
477
- Configuration options for the Threading worker are similar to the threads_on_fork worker, sans the garbage option. When running via the `backburner` CLI, it's simplest to provide the queue names and maximum number of threads in the format "{queue name}:{max threads in pool}[,{name}:{threads}]":
484
+ Configuration options for the Threading worker are similar to the threads_on_fork worker, sans the garbage option. The `backburner` CLI runs the default worker, so set `config.default_worker = Backburner::Workers::Threading` in the app it loads; then it's simplest to provide the queue names and maximum number of threads in the format "{queue name}:{max threads in pool}[,{name}:{threads}]" (the Simple and Forking workers ignore the thread counts):
478
485
 
479
486
  ```
480
487
  $ bundle exec backburner -q queue1:4,queue2:4 # and then other options, like environment, pidfile, app root, etc. See docs for the CLI
@@ -573,9 +580,58 @@ Often using hooks is much easier then trying to monkey patch the externals.
573
580
 
574
581
  Check out [HOOKS.md](https://github.com/nesquena/backburner/blob/master/HOOKS.md) for a detailed overview on using hooks.
575
582
 
583
+ ### Active Job
584
+
585
+ Backburner is an [Active Job](https://guides.rubyonrails.org/active_job_basics.html) adapter. Set it in
586
+ `config/application.rb` (or an environment file):
587
+
588
+ ```ruby
589
+ config.active_job.queue_adapter = :backburner
590
+ ```
591
+
592
+ An Active Job queue is a tube named like any other queue: the `tube_namespace`, the separator and the queue name,
593
+ dasherized. With the namespace `myapp.production`, `queue_as :low_priority` uses the tube `myapp.production.low-priority`;
594
+ a `queue_name_prefix` is part of the queue name (`production_mailers` uses `myapp.production.production-mailers`).
595
+ A job's priority (`queue_with_priority`, `set(priority: 10)`) is its beanstalkd priority (lower runs first), and
596
+ `set(wait: ...)` or `set(wait_until: ...)` delays it (rounded up to whole seconds).
597
+
598
+ Start workers with `-q` listing the Active Job queues, named as Active Job names them (with any prefix):
599
+
600
+ ```
601
+ bundle exec backburner -q default,mailers,low_priority
602
+ ```
603
+
604
+ A worker started without `-q` only watches the tubes that are on the server when it starts (and those of the
605
+ `Backburner::Queue` classes it has loaded, and the primary queue), so the jobs of a queue first used after it started
606
+ aren't worked until it restarts.
607
+
608
+ A job's time to run (TTR) is its class's `queue_respond_timeout` when it defines one, otherwise `config.respond_timeout`
609
+ (120 seconds by default). A job still running when its TTR is up is stopped with `Backburner::Job::JobTimeout`:
610
+
611
+ ```ruby
612
+ class ReportJob < ApplicationJob
613
+ def self.queue_respond_timeout
614
+ 3600 # seconds; subclasses inherit it
615
+ end
616
+ end
617
+ ```
618
+
619
+ Active Job's `retry_on` and Backburner's `max_job_retries` add up: once `retry_on` gives up and the error reaches
620
+ Backburner, it retries the job `max_job_retries` more times before burying it, calling `on_error` each time. Keep
621
+ `max_job_retries` at 0 (the default) for Active Job jobs and retry with `retry_on`, or expect both.
622
+
623
+ Backburner's [hooks](#hooks) don't run for Active Job jobs (they are all enqueued as the adapter's wrapper class):
624
+ use Active Job's callbacks (`before_perform`, `around_perform`, `after_enqueue` and so on) instead. `config.on_error`
625
+ is still called when a job raises.
626
+
627
+ In a Rails app, a worker runs each job (Active Job or not) in the app's reloader, as Rails does: `CurrentAttributes` are
628
+ reset between jobs, a job's database connections go back to the pool when it's done, Active Record's query cache is on
629
+ while a job runs (a job that waits for a change another process makes reads it in `ActiveRecord::Base.uncached { ... }`),
630
+ and in development jobs run the latest code (a reload waits for the jobs running in other threads).
631
+
576
632
  ### Workers in Production
577
633
 
578
- Once you have Backburner setup in your application, starting workers is really easy. Once [beanstalkd](http://kr.github.com/beanstalkd/download.html)
634
+ Once you have Backburner setup in your application, starting workers is really easy. Once [beanstalkd](https://beanstalkd.github.io/download.html)
579
635
  is installed, your best bet is to use the built-in rake task that comes with Backburner. Simply add the task to your Rakefile:
580
636
 
581
637
  ```ruby
@@ -674,8 +730,8 @@ Thanks to these projects for inspiration and certain design and implementation d
674
730
 
675
731
  ## Links
676
732
 
677
- * Code: `git clone git://github.com/nesquena/backburner.git`
678
- * Home: <http://github.com/nesquena/backburner>
679
- * Docs: <http://rdoc.info/github/nesquena/backburner/master/frames>
680
- * Bugs: <http://github.com/nesquena/backburner/issues>
681
- * Gems: <http://gemcutter.org/gems/backburner>
733
+ * Code: `git clone https://github.com/nesquena/backburner.git`
734
+ * Home: <https://github.com/nesquena/backburner>
735
+ * Docs: <https://www.rubydoc.info/gems/backburner>
736
+ * Bugs: <https://github.com/nesquena/backburner/issues>
737
+ * Gems: <https://rubygems.org/gems/backburner>
@@ -15,16 +15,23 @@ module ActiveJob
15
15
  # To use Backburner set the queue_adapter config to +:backburner+.
16
16
  #
17
17
  # Rails.application.config.active_job.queue_adapter = :backburner
18
+ #
19
+ # A job's TTR is its class's +queue_respond_timeout+ when it has one, as for a Backburner::Queue job
20
+ # (+ttr: job.class+ resolves it), else +config.respond_timeout+:
21
+ #
22
+ # class ReportJob < ApplicationJob
23
+ # def self.queue_respond_timeout; 3600; end
24
+ # end
18
25
  class BackburnerAdapter < (rails_7_2_or_newer ? ::ActiveJob::QueueAdapters::AbstractAdapter : Object)
19
26
  def enqueue(job)
20
- response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
27
+ response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, ttr: job.class)
21
28
  job.provider_job_id = response[:id] if response.is_a?(Hash)
22
29
  response
23
30
  end
24
31
 
25
32
  def enqueue_at(job, timestamp)
26
33
  delay = timestamp - Time.current.to_f
27
- response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
34
+ response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay, ttr: job.class)
28
35
  job.provider_job_id = response[:id] if response.is_a?(Hash)
29
36
  response
30
37
  end
@@ -10,7 +10,7 @@ module Backburner
10
10
  opts.on("-r", "--require PATH", String, "The path to load as the environment.") do |req|
11
11
  options[:require] = req
12
12
  end
13
- opts.on("-q", "--queues PATH", String, "The specific queues to work.") do |queues|
13
+ opts.on("-q", "--queues QUEUES", String, "The specific queues to work (comma-separated).") do |queues|
14
14
  options[:queues] = queues
15
15
  end
16
16
  opts.on("-e", "--environment ENVIRONMENT", String, "The environment to run Backburner within") do |environment|
@@ -9,6 +9,8 @@ module Backburner
9
9
  class EnqueuePool
10
10
  MAX_IDLE_CONNECTIONS = 5
11
11
 
12
+ @current = nil
13
+
12
14
  # The pool of this process: a forked child gets a new one and leaves its parent's
13
15
  # sockets (and a lock another thread may have held at the fork) alone.
14
16
  # ceiling: no lock, so threads that race on the first enqueue (or the first after a fork) may
@@ -28,14 +28,16 @@ module Backburner
28
28
  Backburner::AsyncProxy.new(self, nil, opts)
29
29
  end
30
30
 
31
- # Defines perform method for job processing
31
+ # Defines perform method for job processing. It calls a method the app defines on the class,
32
+ # its superclasses or the modules they include, and raises NoMethodError for any other.
32
33
  # @example
33
34
  # perform(55, :do_something, "foo", "bar")
34
35
  def perform(id, method, *args)
35
36
  if id # instance
36
- find(id).send(method, *args)
37
+ record = find(id)
38
+ Backburner::Performable.perform_job_method(record, record.class, Object, method, args)
37
39
  else # class method
38
- send(method, *args)
40
+ Backburner::Performable.perform_job_method(self, singleton_class, Object.singleton_class, method, args)
39
41
  end
40
42
  end # perform
41
43
 
@@ -90,6 +92,29 @@ module Backburner
90
92
  end
91
93
  private_class_method :_handle_asynchronously
92
94
 
95
+ # Calls a job's method on `receiver`. It raises NoMethodError when the method isn't one the app
96
+ # defined: one that `methods_of` (the receiver's class, or the singleton class for a class method)
97
+ # gets from `core` (Object, or Object's singleton class for a class method) or the modules mixed
98
+ # into it, from Backburner's own modules, or Ruby's method_missing hook. A method with the same name
99
+ # as one of core's must be the app's own definition: one that method_missing would handle, or one
100
+ # from Delegator's copy of Kernel (the module delegate.rb aliases __raise__ in), is refused too.
101
+ def self.perform_job_method(receiver, methods_of, core, method, args)
102
+ owner = begin
103
+ methods_of.instance_method(method).owner
104
+ rescue NameError
105
+ nil
106
+ end
107
+ refused = if owner && (core.ancestors.include?(owner) || [InstanceMethods, ClassMethods, Backburner::Queue::ClassMethods].include?(owner))
108
+ true
109
+ elsif core.method_defined?(method) || core.private_method_defined?(method)
110
+ owner.nil? || method.to_sym == :method_missing || (defined?(::Delegator) && owner.equal?(::Delegator.instance_method(:__raise__).owner))
111
+ end
112
+ if refused
113
+ kind = methods_of.singleton_class? ? "class method" : "method"
114
+ raise NoMethodError.new("undefined job #{kind} '#{method}' for #{methods_of.singleton_class? ? receiver : methods_of}", method.to_sym, receiver: receiver)
115
+ end
116
+ receiver.__send__(method, *args)
117
+ end
93
118
 
94
119
  end # Performable
95
120
  end # Backburner
@@ -1,3 +1,3 @@
1
1
  module Backburner
2
- VERSION = "1.8.0"
2
+ VERSION = "1.9.0"
3
3
  end
@@ -73,6 +73,7 @@ module Backburner
73
73
  # @example
74
74
  # Worker.new(['test.job'])
75
75
  def initialize(tube_names=nil)
76
+ @existing_tube_names = nil # set by all_existing_queues, read by expand_tube_name
76
77
  @connection = new_connection
77
78
  @tube_names = self.process_tube_names(tube_names)
78
79
  register_signal_handlers!
@@ -104,9 +105,12 @@ module Backburner
104
105
 
105
106
  # Triggers this worker to shutdown
106
107
  def shutdown
107
- Thread.new do
108
- log_info 'Worker exiting...'
109
- end
108
+ # Logs from another thread, since a trap handler can't take a logger's lock, and waits for it (a
109
+ # second at most): the process exits right after, and an unjoined thread never got to run
110
+ # ceiling: if the signal lands while the main thread holds the logger's lock, the line is lost after a
111
+ # second; handling the signal outside the trap (the self-pipe Threading uses) would lift it
112
+ Thread.new { log_info 'Worker exiting...' }.join(1)
113
+ ensure
110
114
  Kernel.exit
111
115
  end
112
116
 
@@ -136,7 +140,7 @@ module Backburner
136
140
  end
137
141
 
138
142
  self.log_job_begin(job.name, job.args)
139
- job.process
143
+ wrap_in_rails_reloader { job.process }
140
144
  self.log_job_end(job.name)
141
145
 
142
146
  rescue Backburner::Job::JobFormatInvalid => e
@@ -203,6 +207,36 @@ module Backburner
203
207
  Connection.new(Backburner.configuration.beanstalk_url) { |conn| Backburner::Hooks.invoke_hook_events(self, :on_reconnect, conn) }
204
208
  end
205
209
 
210
+ # Runs the block (a job's processing) in the Rails app's reloader when there is one, as Rails does for Active Job
211
+ # jobs (whose own wrap, inside this one, then does nothing): each job starts with fresh CurrentAttributes, gives
212
+ # back its database connections when it's done and, in development, runs the app's latest code. On Rails 7.0+ the
213
+ # wrap reports the errors that leave it to Rails.error (with the source "job.backburner" on 7.1+); a RetryJob, a
214
+ # request to retry rather than a failure, and a SystemExit (a TERM or INT the worker stops on, or a job calling
215
+ # exit), which Rails 7.2+ would report too, are raised again outside it instead.
216
+ # ceiling: config.on_error and the on_retry and on_bury hooks run after the wrap ends, so they don't see the job's
217
+ # CurrentAttributes, and a database connection they use is outside the executor; running work_one_job's failure
218
+ # handling inside the wrap would lift it. In development, a Forking or ThreadsOnFork parent never runs the wrap, so
219
+ # after a file change each child reloads the app once.
220
+ def wrap_in_rails_reloader
221
+ app = ::Rails.application if defined?(::Rails) && ::Rails.respond_to?(:application)
222
+ return yield unless app.respond_to?(:reloader)
223
+ deferred = nil
224
+ work = proc do
225
+ begin
226
+ yield
227
+ rescue Backburner::Job::RetryJob, SystemExit => e
228
+ deferred = e
229
+ end
230
+ end
231
+ # Active Support 7.1+ take the source the executor reports errors with; older ones take no options
232
+ if ActiveSupport.gem_version >= Gem::Version.new("7.1")
233
+ app.reloader.wrap(source: "job.backburner", &work)
234
+ else
235
+ app.reloader.wrap(&work)
236
+ end
237
+ raise deferred if deferred
238
+ end
239
+
206
240
  # Reserve a job from the watched queues
207
241
  def reserve_job(conn, reserve_timeout = Backburner.configuration.reserve_timeout)
208
242
  Backburner::Job.new(conn.tubes.reserve(reserve_timeout))
@@ -7,6 +7,9 @@ module Backburner
7
7
 
8
8
  @shutdown_timeout = 10
9
9
 
10
+ # The longest a pool thread waits in reserve at a time (beanstalkd takes whole seconds)
11
+ SHUTDOWN_CHECK_INTERVAL = 1
12
+
10
13
  class << self
11
14
  attr_accessor :threads_number
12
15
  attr_accessor :shutdown_timeout
@@ -15,6 +18,7 @@ module Backburner
15
18
  # Custom initializer just to set @tubes_data
16
19
  def initialize(*args)
17
20
  @tubes_data = {}
21
+ @in_shutdown = false
18
22
  super
19
23
  self.process_tube_options
20
24
  @exit_on_shutdown = true
@@ -152,6 +156,18 @@ module Backburner
152
156
  end
153
157
  end
154
158
  end
159
+
160
+ protected
161
+
162
+ # Reserves for at most SHUTDOWN_CHECK_INTERVAL seconds, so an idle thread sees a shutdown
163
+ # and stops instead of waiting in reserve until shutdown_timeout kills it
164
+ # ceiling: each idle thread sends beanstalkd a reserve once a second; waking the waiting
165
+ # reserves at shutdown instead would avoid that polling
166
+ def reserve_job(conn, reserve_timeout = Backburner.configuration.reserve_timeout)
167
+ timeout = reserve_timeout.to_s.to_i if reserve_timeout # as beanstalkd reads what beaneater sends: "5", 2.5, 5.seconds
168
+ timeout = SHUTDOWN_CHECK_INTERVAL if timeout.nil? || timeout < 0 || timeout > SHUTDOWN_CHECK_INTERVAL
169
+ super(conn, timeout)
170
+ end
155
171
  end # Threading
156
172
  end # Workers
157
173
  end # Backburner
@@ -69,6 +69,7 @@ module Backburner
69
69
  # Custom initializer just to set @tubes_data
70
70
  def initialize(*args)
71
71
  @tubes_data = {}
72
+ @watching_tube = nil
72
73
  super
73
74
  self.process_tube_options
74
75
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backburner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Esquenazi
@@ -164,27 +164,11 @@ executables:
164
164
  extensions: []
165
165
  extra_rdoc_files: []
166
166
  files:
167
- - ".github/workflows/ci.yml"
168
- - ".github/workflows/github-release.yml"
169
- - ".github/workflows/release.yml"
170
- - ".gitignore"
171
167
  - CHANGELOG.md
172
- - CONTRIBUTING.md
173
- - Gemfile
174
168
  - HOOKS.md
175
169
  - LICENSE
176
170
  - README.md
177
- - Rakefile
178
- - TODO
179
- - backburner.gemspec
180
171
  - bin/backburner
181
- - examples/custom.rb
182
- - examples/demo.rb
183
- - examples/god.rb
184
- - examples/hooked.rb
185
- - examples/retried.rb
186
- - examples/simple.rb
187
- - examples/stress.rb
188
172
  - lib/active_job/queue_adapters/backburner_adapter.rb
189
173
  - lib/backburner.rb
190
174
  - lib/backburner/async_proxy.rb
@@ -205,41 +189,14 @@ files:
205
189
  - lib/backburner/workers/simple.rb
206
190
  - lib/backburner/workers/threading.rb
207
191
  - lib/backburner/workers/threads_on_fork.rb
208
- - test/active_job_adapter_test.rb
209
- - test/async_proxy_test.rb
210
- - test/back_burner_test.rb
211
- - test/connection_test.rb
212
- - test/fixtures/active_job_app.rb
213
- - test/fixtures/active_jobs.rb
214
- - test/fixtures/autoload/test_broken_name_job.rb
215
- - test/fixtures/autoload/test_broken_no_method_job.rb
216
- - test/fixtures/autoload/test_broken_require_job.rb
217
- - test/fixtures/autoload/test_broken_runtime_job.rb
218
- - test/fixtures/autoload/test_broken_syntax_job.rb
219
- - test/fixtures/autoload/test_broken_timeout_job.rb
220
- - test/fixtures/autoload/test_same_name_job.rb
221
- - test/fixtures/hooked.rb
222
- - test/fixtures/test_fork_jobs.rb
223
- - test/fixtures/test_forking_jobs.rb
224
- - test/fixtures/test_jobs.rb
225
- - test/fixtures/test_queue_settings.rb
226
- - test/helpers/templogger.rb
227
- - test/helpers_test.rb
228
- - test/hooks_test.rb
229
- - test/job_test.rb
230
- - test/logger_test.rb
231
- - test/performable_test.rb
232
- - test/queue_test.rb
233
- - test/test_helper.rb
234
- - test/worker_test.rb
235
- - test/workers/forking_worker_test.rb
236
- - test/workers/simple_worker_test.rb
237
- - test/workers/threading_worker_test.rb
238
- - test/workers/threads_on_fork_worker_test.rb
239
- homepage: http://github.com/nesquena/backburner
192
+ homepage: https://github.com/nesquena/backburner
240
193
  licenses:
241
194
  - MIT
242
- metadata: {}
195
+ metadata:
196
+ source_code_uri: https://github.com/nesquena/backburner
197
+ changelog_uri: https://github.com/nesquena/backburner/blob/master/CHANGELOG.md
198
+ bug_tracker_uri: https://github.com/nesquena/backburner/issues
199
+ rubygems_mfa_required: 'true'
243
200
  post_install_message:
244
201
  rdoc_options: []
245
202
  require_paths:
@@ -259,35 +216,4 @@ rubygems_version: 3.5.22
259
216
  signing_key:
260
217
  specification_version: 4
261
218
  summary: Reliable beanstalk background job processing made easy for Ruby and Sinatra
262
- test_files:
263
- - test/active_job_adapter_test.rb
264
- - test/async_proxy_test.rb
265
- - test/back_burner_test.rb
266
- - test/connection_test.rb
267
- - test/fixtures/active_job_app.rb
268
- - test/fixtures/active_jobs.rb
269
- - test/fixtures/autoload/test_broken_name_job.rb
270
- - test/fixtures/autoload/test_broken_no_method_job.rb
271
- - test/fixtures/autoload/test_broken_require_job.rb
272
- - test/fixtures/autoload/test_broken_runtime_job.rb
273
- - test/fixtures/autoload/test_broken_syntax_job.rb
274
- - test/fixtures/autoload/test_broken_timeout_job.rb
275
- - test/fixtures/autoload/test_same_name_job.rb
276
- - test/fixtures/hooked.rb
277
- - test/fixtures/test_fork_jobs.rb
278
- - test/fixtures/test_forking_jobs.rb
279
- - test/fixtures/test_jobs.rb
280
- - test/fixtures/test_queue_settings.rb
281
- - test/helpers/templogger.rb
282
- - test/helpers_test.rb
283
- - test/hooks_test.rb
284
- - test/job_test.rb
285
- - test/logger_test.rb
286
- - test/performable_test.rb
287
- - test/queue_test.rb
288
- - test/test_helper.rb
289
- - test/worker_test.rb
290
- - test/workers/forking_worker_test.rb
291
- - test/workers/simple_worker_test.rb
292
- - test/workers/threading_worker_test.rb
293
- - test/workers/threads_on_fork_worker_test.rb
219
+ test_files: []
@@ -1,45 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ "master" ]
6
- pull_request:
7
- branches: [ "master" ]
8
-
9
- permissions:
10
- contents: read
11
-
12
- jobs:
13
- test:
14
- name: test (${{ matrix.ruby-version }}${{ matrix.activejob-version && format(', activejob {0}', matrix.activejob-version) || '' }})
15
- runs-on: ubuntu-latest
16
- strategy:
17
- fail-fast: false
18
- matrix:
19
- ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
20
- activejob-version: [''] # the newest the Ruby version allows
21
- include:
22
- # Rails before 7.2 ship their own Active Job adapter and have no AbstractAdapter
23
- - ruby-version: '3.1'
24
- activejob-version: '6.1'
25
- env:
26
- ACTIVEJOB_VERSION: ${{ matrix.activejob-version }} # read by the Gemfile
27
-
28
- steps:
29
- - uses: actions/checkout@v4
30
- - name: Download & Extract beanstalkd
31
- run: curl -fsSL https://github.com/beanstalkd/beanstalkd/archive/refs/tags/v1.13.tar.gz | tar xz
32
- - name: Make beanstalkd
33
- run: make
34
- working-directory: beanstalkd-1.13
35
- - name: Set up Ruby
36
- uses: ruby/setup-ruby@v1
37
- with:
38
- ruby-version: ${{ matrix.ruby-version }}
39
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
40
- - name: Daemonize beanstalkd
41
- run: ./beanstalkd-1.13/beanstalkd -l 127.0.0.1 -p 11300 &
42
- - name: Run tests
43
- run: bundle exec rake test
44
- env:
45
- BEANSTALKD_BIN: ${{ github.workspace }}/beanstalkd-1.13/beanstalkd # for the outage test, which stops and restarts its own server