good_job 1.0.3 → 1.1.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 +21 -2
- data/README.md +113 -21
- data/lib/active_job/queue_adapters/good_job_adapter.rb +20 -3
- data/lib/good_job.rb +3 -2
- data/lib/good_job/adapter.rb +26 -8
- data/lib/good_job/cli.rb +18 -15
- data/lib/good_job/job.rb +6 -2
- data/lib/good_job/log_subscriber.rb +110 -0
- data/lib/good_job/performer.rb +4 -1
- data/lib/good_job/railtie.rb +1 -0
- data/lib/good_job/scheduler.rb +41 -12
- data/lib/good_job/version.rb +1 -1
- metadata +34 -6
- data/lib/good_job/logging.rb +0 -70
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c6d495a4455453f6f4af736d3fd31685fc2026c98e0cd564ea0f47b188cc473
|
4
|
+
data.tar.gz: 794137e732ed3fcebec859daa8e58a2c2c8fd283cc43c5cfc7824263a1ca93fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 258757117262f25f5507ceb47c20f0f43a1f40427c7a0f9c3d0dc7803f25019062a160de08e96fd5ba74ceeeec5666d1084f6dff7ca9d381b2bbb198fabb10fd
|
7
|
+
data.tar.gz: e2a5d09cfb57a7f2a08d59f84c3b39e3582cb5b0b76a7e6ea2da421542d5e0ebee2e25a79298c1f5d3351d58e156e43ac0770e1d9e26e0f26169620f0bfcc493
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,25 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [v1.0
|
3
|
+
## [v1.1.0](https://github.com/bensheldon/good_job/tree/v1.1.0) (2020-08-09)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.3...v1.1.0)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- Document reliability guarantees [\#59](https://github.com/bensheldon/good_job/issues/59)
|
10
|
+
- Document how to hook in exception monitor \(Sentry, Rollbar, etc\) [\#47](https://github.com/bensheldon/good_job/issues/47)
|
11
|
+
- Allow an Async mode [\#27](https://github.com/bensheldon/good_job/issues/27)
|
12
|
+
|
13
|
+
**Merged pull requests:**
|
14
|
+
|
15
|
+
- Add a callable hook on thread errors [\#71](https://github.com/bensheldon/good_job/pull/71) ([bensheldon](https://github.com/bensheldon))
|
16
|
+
- Clarify reliability guarantees [\#70](https://github.com/bensheldon/good_job/pull/70) ([bensheldon](https://github.com/bensheldon))
|
17
|
+
- Clean up Readme formatting; re-arrange tests for clarity and values [\#69](https://github.com/bensheldon/good_job/pull/69) ([bensheldon](https://github.com/bensheldon))
|
18
|
+
- Create an Async execution mode [\#68](https://github.com/bensheldon/good_job/pull/68) ([bensheldon](https://github.com/bensheldon))
|
19
|
+
- Move all stdout to LogSubscriber [\#67](https://github.com/bensheldon/good_job/pull/67) ([bensheldon](https://github.com/bensheldon))
|
20
|
+
- Allow schedulers to be restarted; separate unit tests from integration tests [\#66](https://github.com/bensheldon/good_job/pull/66) ([bensheldon](https://github.com/bensheldon))
|
21
|
+
|
22
|
+
## [v1.0.3](https://github.com/bensheldon/good_job/tree/v1.0.3) (2020-07-26)
|
4
23
|
|
5
24
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.2...v1.0.3)
|
6
25
|
|
@@ -113,7 +132,6 @@
|
|
113
132
|
- Add configuration options to good\_job executable [\#33](https://github.com/bensheldon/good_job/pull/33) ([bensheldon](https://github.com/bensheldon))
|
114
133
|
- Extract Job querying behavior out of Scheduler [\#31](https://github.com/bensheldon/good_job/pull/31) ([bensheldon](https://github.com/bensheldon))
|
115
134
|
- Allow configuration of Rails queue adapter with `:good\_job` [\#28](https://github.com/bensheldon/good_job/pull/28) ([bensheldon](https://github.com/bensheldon))
|
116
|
-
- Update development Ruby to 2.6.5 [\#22](https://github.com/bensheldon/good_job/pull/22) ([bensheldon](https://github.com/bensheldon))
|
117
135
|
|
118
136
|
## [v0.5.0](https://github.com/bensheldon/good_job/tree/v0.5.0) (2020-07-13)
|
119
137
|
|
@@ -138,6 +156,7 @@
|
|
138
156
|
|
139
157
|
**Merged pull requests:**
|
140
158
|
|
159
|
+
- Update development Ruby to 2.6.5 [\#22](https://github.com/bensheldon/good_job/pull/22) ([bensheldon](https://github.com/bensheldon))
|
141
160
|
- Simplify the internal API, removing JobWrapper and InlineScheduler [\#21](https://github.com/bensheldon/good_job/pull/21) ([bensheldon](https://github.com/bensheldon))
|
142
161
|
- Generate a new future for every executed job [\#20](https://github.com/bensheldon/good_job/pull/20) ([bensheldon](https://github.com/bensheldon))
|
143
162
|
- Configuration for maximum number of job execution threads [\#18](https://github.com/bensheldon/good_job/pull/18) ([bensheldon](https://github.com/bensheldon))
|
data/README.md
CHANGED
@@ -27,7 +27,8 @@ $ bundle install
|
|
27
27
|
## Usage
|
28
28
|
|
29
29
|
1. Create a database migration:
|
30
|
-
|
30
|
+
|
31
|
+
```bash
|
31
32
|
$ bin/rails g good_job:install
|
32
33
|
```
|
33
34
|
|
@@ -38,7 +39,8 @@ $ bundle install
|
|
38
39
|
```
|
39
40
|
|
40
41
|
1. Configure the ActiveJob adapter:
|
41
|
-
|
42
|
+
|
43
|
+
```ruby
|
42
44
|
# config/application.rb
|
43
45
|
config.active_job.queue_adapter = :good_job
|
44
46
|
```
|
@@ -57,16 +59,19 @@ $ bundle install
|
|
57
59
|
```
|
58
60
|
|
59
61
|
1. Queue your job 🎉:
|
62
|
+
|
60
63
|
```ruby
|
61
64
|
YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later
|
62
65
|
```
|
63
66
|
|
64
67
|
1. In production, the scheduler is designed to run in its own process:
|
68
|
+
|
65
69
|
```bash
|
66
70
|
$ bundle exec good_job
|
67
71
|
```
|
68
72
|
|
69
73
|
Configuration options available with `help`:
|
74
|
+
|
70
75
|
```bash
|
71
76
|
$ bundle exec good_job help start
|
72
77
|
|
@@ -81,7 +86,7 @@ $ bundle install
|
|
81
86
|
|
82
87
|
### Error handling, retries, and reliability
|
83
88
|
|
84
|
-
GoodJob guarantees
|
89
|
+
GoodJob guarantees that a completely-performed job will run once and only once. GoodJob fully supports ActiveJob's built-in functionality for error handling, retries and timeouts. Writing reliable, transactional, and idempotent `ActiveJob#perform` methods is outside the scope of GoodJob.
|
85
90
|
|
86
91
|
#### Error handling
|
87
92
|
|
@@ -95,6 +100,15 @@ By default, if a job raises an error while it is being performed, _and it bubble
|
|
95
100
|
GoodJob.reperform_jobs_on_standard_error = true # => default
|
96
101
|
```
|
97
102
|
|
103
|
+
To report errors that _do_ bubble up to the GoodJob backend, assign a callable to `GoodJob.on_thread_error`. For example:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
# config/initializers/good_job.rb
|
107
|
+
|
108
|
+
# With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
|
109
|
+
GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
|
110
|
+
```
|
111
|
+
|
98
112
|
### Retrying jobs
|
99
113
|
|
100
114
|
ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. Using ActiveJob's `retry_on` will ensure that errors do not bubble up to the GoodJob backend:
|
@@ -123,12 +137,29 @@ end
|
|
123
137
|
|
124
138
|
GoodJob can be configured to allow omitting `retry_on`'s block argument and implicitly discard un-handled errors:
|
125
139
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
140
|
+
```ruby
|
141
|
+
# config/initializers/good_job.rb
|
142
|
+
|
143
|
+
# Do NOT re-perform a job if a StandardError bubbles up to the GoodJob backend
|
144
|
+
GoodJob.reperform_jobs_on_standard_error = false
|
145
|
+
```
|
146
|
+
|
147
|
+
When using an exception monitoring service (e.g. Sentry, Bugsnag, Airbrake, Honeybadger, etc), the use of `rescue_on` may be incompatible with their ActiveJob integration. It's safest to explicitly wrap jobs with an exception reporter. For example:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class ApplicationJob < ActiveJob::Base
|
151
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
|
152
|
+
|
153
|
+
around_perform do |_job, block|
|
154
|
+
block.call
|
155
|
+
rescue StandardError => e
|
156
|
+
Raven.capture_exception(e)
|
157
|
+
raise
|
158
|
+
end
|
159
|
+
# ...
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
132
163
|
|
133
164
|
ActiveJob's `discard_on` functionality is supported too.
|
134
165
|
|
@@ -139,6 +170,14 @@ Using a Mailer's `#deliver_later` will enqueue an instance of `ActionMailer::Del
|
|
139
170
|
```ruby
|
140
171
|
# config/initializers/good_job.rb
|
141
172
|
ActionMailer::DeliveryJob.retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
|
173
|
+
|
174
|
+
# With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
|
175
|
+
ActionMailer::DeliveryJob.around_perform do |_job, block|
|
176
|
+
block.call
|
177
|
+
rescue StandardError => e
|
178
|
+
Raven.capture_exception(e)
|
179
|
+
raise
|
180
|
+
end
|
142
181
|
```
|
143
182
|
|
144
183
|
#### Timeouts
|
@@ -158,7 +197,7 @@ class ApplicationJob < ActiveJob::Base
|
|
158
197
|
end
|
159
198
|
```
|
160
199
|
|
161
|
-
### Configuring
|
200
|
+
### Configuring job execution threads
|
162
201
|
|
163
202
|
GoodJob executes enqueued jobs using threads. There is a lot than can be said about [multithreaded behavior in Ruby on Rails](https://guides.rubyonrails.org/threading_and_code_execution.html), but briefly:
|
164
203
|
|
@@ -169,6 +208,51 @@ GoodJob executes enqueued jobs using threads. There is a lot than can be said ab
|
|
169
208
|
3. `$ RAILS_MAX_THREADS=4 bundle exec good_job`
|
170
209
|
4. Implicitly via Rails's database connection pool size (`ActiveRecord::Base.connection_pool.size`)
|
171
210
|
|
211
|
+
### Executing jobs async / in-process
|
212
|
+
|
213
|
+
GoodJob is able to run "async" in the same process as the webserver (e.g. `bin/rail s`). GoodJob's async execution mode offers benefits of economy by not requiring a separate job worker process, but with the tradeoff of increased complexity. Async mode can be configured in two ways:
|
214
|
+
|
215
|
+
- Directly configure the ActiveJob adapter:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
# config/environments/production.rb
|
219
|
+
config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :async, max_threads: 4, poll_interval: 30)
|
220
|
+
```
|
221
|
+
- Or, when using `...queue_adapter = :good_job`, via environment variables:
|
222
|
+
|
223
|
+
```bash
|
224
|
+
$ GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server
|
225
|
+
```
|
226
|
+
|
227
|
+
Depending on your application configuration, you may need to take additional steps:
|
228
|
+
|
229
|
+
- Ensure that you have enough database connections for both web and job execution threads:
|
230
|
+
|
231
|
+
```yaml
|
232
|
+
# config/database.yml
|
233
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + ENV.fetch("GOOD_JOB_MAX_THREADS", 4).to_i %>
|
234
|
+
```
|
235
|
+
|
236
|
+
- When running Puma with workers (`WEB_CONCURRENCY > 0`) or another process-forking webserver, GoodJob's threadpool schedulers should be stopped before forking, restarted after fork, and cleanly shut down on exit. Stopping GoodJob's scheduler pre-fork is recommended to ensure that GoodJob does not continue executing jobs in the parent/controller process. For example, with Puma:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# config/puma.rb
|
240
|
+
|
241
|
+
before_fork do
|
242
|
+
GoodJob::Scheduler.instances.each { |s| s.shutdown }
|
243
|
+
end
|
244
|
+
|
245
|
+
on_worker_boot do
|
246
|
+
GoodJob::Scheduler.instances.each { |s| s.restart }
|
247
|
+
end
|
248
|
+
|
249
|
+
on_worker_shutdown do
|
250
|
+
GoodJob::Scheduler.instances.each { |s| s.shutdown }
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
GoodJob is compatible with Puma's `preload_app!` method.
|
255
|
+
|
172
256
|
### Migrating to GoodJob from a different ActiveJob backend
|
173
257
|
|
174
258
|
If your application is already using an ActiveJob backend, you will need to install GoodJob to enqueue and perform newly created jobs _and_ finish performing pre-existing jobs on the previous backend.
|
@@ -184,7 +268,8 @@ If your application is already using an ActiveJob backend, you will need to inst
|
|
184
268
|
```
|
185
269
|
|
186
270
|
1. Continue running executors for both backends. For example, on Heroku it's possible to run [two processes](https://help.heroku.com/CTFS2TJK/how-do-i-run-multiple-processes-on-a-dyno) within the same dyno:
|
187
|
-
|
271
|
+
|
272
|
+
```procfile
|
188
273
|
# Procfile
|
189
274
|
# ...
|
190
275
|
worker: bundle exec que ./config/environment.rb & bundle exec good_job & wait -n
|
@@ -210,15 +295,24 @@ It is also necessary to delete these preserved jobs from the database after a ce
|
|
210
295
|
- For example, in a Rake task:
|
211
296
|
|
212
297
|
```ruby
|
213
|
-
|
298
|
+
GoodJob::Job.finished(1.day.ago).delete_all
|
214
299
|
```
|
300
|
+
|
215
301
|
- For example, using the `good_job` command-line utility:
|
216
302
|
|
217
303
|
```bash
|
218
304
|
$ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
|
219
305
|
```
|
220
306
|
|
221
|
-
##
|
307
|
+
## Contributing
|
308
|
+
|
309
|
+
Contributions are welcomed and appreciated 🙏
|
310
|
+
|
311
|
+
- Review the [Prioritized Project Backlog](https://github.com/bensheldon/good_job/projects/1).
|
312
|
+
- Open a new Issue or contribute to an [existing Issue](https://github.com/bensheldon/good_job/issues). Questions or suggestions are fantastic.
|
313
|
+
- Participate according to our [Code of Conduct](https://github.com/bensheldon/good_job/projects/1).
|
314
|
+
|
315
|
+
### Gem development
|
222
316
|
|
223
317
|
To run tests:
|
224
318
|
|
@@ -241,7 +335,6 @@ $ bundle exec appraisal
|
|
241
335
|
|
242
336
|
# Run tests
|
243
337
|
$ bundle exec appraisal bin/rspec
|
244
|
-
|
245
338
|
```
|
246
339
|
|
247
340
|
For developing locally within another Ruby on Rails project:
|
@@ -256,24 +349,23 @@ $ bundle install
|
|
256
349
|
# => Using good_job 0.1.0 from https://github.com/bensheldon/good_job.git (at /Users/You/Projects/good_job@dc57fb0)
|
257
350
|
```
|
258
351
|
|
259
|
-
|
352
|
+
### Releasing
|
260
353
|
|
261
|
-
Package maintainers can release this gem
|
354
|
+
Package maintainers can release this gem by running:
|
262
355
|
|
263
356
|
```bash
|
264
357
|
# Sign into rubygems
|
265
358
|
$ gem signin
|
266
359
|
|
360
|
+
# Add a .env file with the following:
|
361
|
+
# CHANGELOG_GITHUB_TOKEN= # Github Personal Access Token
|
362
|
+
|
267
363
|
# Update version number, changelog, and create git commit:
|
268
|
-
$ bundle exec rake
|
364
|
+
$ bundle exec rake release[minor] # major,minor,patch
|
269
365
|
|
270
366
|
# ..and follow subsequent directions.
|
271
367
|
```
|
272
368
|
|
273
|
-
## Contributing
|
274
|
-
|
275
|
-
Contribution directions go here.
|
276
|
-
|
277
369
|
## License
|
278
370
|
|
279
371
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -1,18 +1,35 @@
|
|
1
1
|
module ActiveJob
|
2
2
|
module QueueAdapters
|
3
3
|
class GoodJobAdapter < GoodJob::Adapter
|
4
|
-
def initialize(execution_mode: nil)
|
4
|
+
def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil)
|
5
5
|
execution_mode = if execution_mode
|
6
6
|
execution_mode
|
7
7
|
elsif ENV['GOOD_JOB_EXECUTION_MODE'].present?
|
8
8
|
ENV['GOOD_JOB_EXECUTION_MODE'].to_sym
|
9
|
-
elsif Rails.env.development?
|
9
|
+
elsif Rails.env.development?
|
10
|
+
:inline
|
11
|
+
elsif Rails.env.test?
|
10
12
|
:inline
|
11
13
|
else
|
12
14
|
:external
|
13
15
|
end
|
14
16
|
|
15
|
-
|
17
|
+
if execution_mode == :async && scheduler.blank?
|
18
|
+
max_threads = (
|
19
|
+
max_threads.presence ||
|
20
|
+
ENV['GOOD_JOB_MAX_THREADS'] ||
|
21
|
+
ENV['RAILS_MAX_THREADS'] ||
|
22
|
+
ActiveRecord::Base.connection_pool.size
|
23
|
+
).to_i
|
24
|
+
|
25
|
+
poll_interval = (
|
26
|
+
poll_interval.presence ||
|
27
|
+
ENV['GOOD_JOB_POLL_INTERVAL'] ||
|
28
|
+
1
|
29
|
+
).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
super(execution_mode: execution_mode, max_threads: max_threads, poll_interval: poll_interval, scheduler: scheduler)
|
16
33
|
end
|
17
34
|
end
|
18
35
|
end
|
data/lib/good_job.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "rails"
|
2
2
|
require 'good_job/railtie'
|
3
3
|
|
4
|
-
require 'good_job/
|
4
|
+
require 'good_job/log_subscriber'
|
5
5
|
require 'good_job/lockable'
|
6
6
|
require 'good_job/job'
|
7
7
|
require 'good_job/scheduler'
|
@@ -12,9 +12,10 @@ require 'good_job/performer'
|
|
12
12
|
require 'active_job/queue_adapters/good_job_adapter'
|
13
13
|
|
14
14
|
module GoodJob
|
15
|
+
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
15
16
|
mattr_accessor :preserve_job_records, default: false
|
16
17
|
mattr_accessor :reperform_jobs_on_standard_error, default: true
|
17
|
-
|
18
|
+
mattr_accessor :on_thread_error, default: nil
|
18
19
|
|
19
20
|
ActiveSupport.run_load_hooks(:good_job, self)
|
20
21
|
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module GoodJob
|
2
2
|
class Adapter
|
3
|
-
EXECUTION_MODES = [:
|
3
|
+
EXECUTION_MODES = [:async, :external, :inline].freeze
|
4
4
|
|
5
|
-
def initialize(execution_mode: nil, inline: false)
|
5
|
+
def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
|
6
6
|
if inline
|
7
7
|
ActiveSupport::Deprecation.warn('GoodJob::Adapter#new(inline: true) is deprecated; use GoodJob::Adapter.new(execution_mode: :inline) instead')
|
8
8
|
@execution_mode = :inline
|
@@ -13,6 +13,18 @@ module GoodJob
|
|
13
13
|
else
|
14
14
|
@execution_mode = :external
|
15
15
|
end
|
16
|
+
|
17
|
+
@scheduler = scheduler
|
18
|
+
if @execution_mode == :async && @scheduler.blank? # rubocop:disable Style/GuardClause
|
19
|
+
timer_options = {}
|
20
|
+
timer_options[:execution_interval] = poll_interval if poll_interval.present?
|
21
|
+
|
22
|
+
pool_options = {}
|
23
|
+
pool_options[:max_threads] = max_threads if max_threads.present?
|
24
|
+
|
25
|
+
job_performer = GoodJob::Performer.new(GoodJob::Job, :perform_with_advisory_lock, name: '*')
|
26
|
+
@scheduler = GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
|
27
|
+
end
|
16
28
|
end
|
17
29
|
|
18
30
|
def enqueue(active_job)
|
@@ -34,11 +46,21 @@ module GoodJob
|
|
34
46
|
end
|
35
47
|
end
|
36
48
|
|
49
|
+
@scheduler.create_thread if execute_async?
|
50
|
+
|
37
51
|
good_job
|
38
52
|
end
|
39
53
|
|
40
|
-
def shutdown(wait: true)
|
41
|
-
|
54
|
+
def shutdown(wait: true)
|
55
|
+
@scheduler&.shutdown(wait: wait)
|
56
|
+
end
|
57
|
+
|
58
|
+
def execute_async?
|
59
|
+
@execution_mode == :async
|
60
|
+
end
|
61
|
+
|
62
|
+
def execute_externally?
|
63
|
+
@execution_mode == :external
|
42
64
|
end
|
43
65
|
|
44
66
|
def execute_inline?
|
@@ -49,9 +71,5 @@ module GoodJob
|
|
49
71
|
ActiveSupport::Deprecation.warn('GoodJob::Adapter::inline? is deprecated; use GoodJob::Adapter::execute_inline? instead')
|
50
72
|
execute_inline?
|
51
73
|
end
|
52
|
-
|
53
|
-
def execute_externally?
|
54
|
-
@execution_mode == :external
|
55
|
-
end
|
56
74
|
end
|
57
75
|
end
|
data/lib/good_job/cli.rb
CHANGED
@@ -16,7 +16,7 @@ module GoodJob
|
|
16
16
|
type: :numeric,
|
17
17
|
desc: "Interval between polls for available jobs in seconds (default: 1)"
|
18
18
|
def start
|
19
|
-
|
19
|
+
set_up_application!
|
20
20
|
|
21
21
|
max_threads = (
|
22
22
|
options[:max_threads] ||
|
@@ -25,23 +25,19 @@ module GoodJob
|
|
25
25
|
ActiveRecord::Base.connection_pool.size
|
26
26
|
).to_i
|
27
27
|
|
28
|
-
|
28
|
+
queue_string = (
|
29
29
|
options[:queues] ||
|
30
30
|
ENV['GOOD_JOB_QUEUES'] ||
|
31
31
|
'*'
|
32
|
-
)
|
32
|
+
)
|
33
33
|
|
34
34
|
poll_interval = (
|
35
35
|
options[:poll_interval] ||
|
36
36
|
ENV['GOOD_JOB_POLL_INTERVAL']
|
37
37
|
).to_i
|
38
38
|
|
39
|
-
job_query = GoodJob::Job.
|
40
|
-
|
41
|
-
job_query = job_query.where(queue_name: queue_names_without_all) unless queue_names_without_all.size.zero?
|
42
|
-
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock)
|
43
|
-
|
44
|
-
$stdout.puts "GoodJob worker starting with max_threads=#{max_threads} on queues=#{queue_names.join(',')}"
|
39
|
+
job_query = GoodJob::Job.queue_string(queue_string)
|
40
|
+
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
|
45
41
|
|
46
42
|
timer_options = {}
|
47
43
|
timer_options[:execution_interval] = poll_interval if poll_interval.positive?
|
@@ -62,24 +58,31 @@ module GoodJob
|
|
62
58
|
break if @stop_good_job_executable || scheduler.shutdown?
|
63
59
|
end
|
64
60
|
|
65
|
-
$stdout.puts "\nFinishing GoodJob's current jobs before exiting..."
|
66
61
|
scheduler.shutdown
|
67
|
-
$stdout.puts "GoodJob's jobs finished, exiting..."
|
68
62
|
end
|
69
63
|
|
64
|
+
default_task :start
|
65
|
+
|
70
66
|
desc :cleanup_preserved_jobs, "Delete preserved job records"
|
71
67
|
method_option :before_seconds_ago,
|
72
68
|
type: :numeric,
|
73
69
|
default: 24 * 60 * 60,
|
74
70
|
desc: "Delete records finished more than this many seconds ago"
|
75
71
|
def cleanup_preserved_jobs
|
76
|
-
|
72
|
+
set_up_application!
|
77
73
|
|
78
74
|
timestamp = Time.current - options[:before_seconds_ago]
|
79
|
-
|
80
|
-
|
75
|
+
ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { before_seconds_ago: options[:before_seconds_ago], timestamp: timestamp }) do |payload|
|
76
|
+
deleted_records_count = GoodJob::Job.finished(timestamp).delete_all
|
77
|
+
|
78
|
+
payload[:deleted_records_count] = deleted_records_count
|
79
|
+
end
|
81
80
|
end
|
82
81
|
|
83
|
-
|
82
|
+
no_commands do
|
83
|
+
def set_up_application!
|
84
|
+
require RAILS_ENVIRONMENT_RB
|
85
|
+
end
|
86
|
+
end
|
84
87
|
end
|
85
88
|
end
|
data/lib/good_job/job.rb
CHANGED
@@ -18,15 +18,19 @@ module GoodJob
|
|
18
18
|
end
|
19
19
|
end)
|
20
20
|
scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
|
21
|
-
scope :priority_ordered, -> { order(priority
|
21
|
+
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
22
22
|
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
|
23
|
+
scope :queue_string, (lambda do |string|
|
24
|
+
queue_names_without_all = (string.presence || '*').split(',').map(&:strip).reject { |q| q == '*' }
|
25
|
+
where(queue_name: queue_names_without_all) unless queue_names_without_all.size.zero?
|
26
|
+
end)
|
23
27
|
|
24
28
|
def self.perform_with_advisory_lock
|
25
29
|
good_job = nil
|
26
30
|
result = nil
|
27
31
|
error = nil
|
28
32
|
|
29
|
-
unfinished.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
33
|
+
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
30
34
|
good_job = good_jobs.first
|
31
35
|
break unless good_job
|
32
36
|
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module GoodJob
|
2
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
3
|
+
def create(event)
|
4
|
+
good_job = event.payload[:good_job]
|
5
|
+
|
6
|
+
debug do
|
7
|
+
"GoodJob created job resource with id #{good_job.id}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def timer_task_finished(event)
|
12
|
+
exception = event.payload[:error]
|
13
|
+
return unless exception
|
14
|
+
|
15
|
+
error do
|
16
|
+
"GoodJob error: #{exception}\n #{exception.backtrace}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def job_finished(event)
|
21
|
+
exception = event.payload[:error]
|
22
|
+
return unless exception
|
23
|
+
|
24
|
+
error do
|
25
|
+
"GoodJob error: #{exception}\n #{exception.backtrace}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def scheduler_create_pools(event)
|
30
|
+
max_threads = event.payload[:max_threads]
|
31
|
+
poll_interval = event.payload[:poll_interval]
|
32
|
+
performer_name = event.payload[:performer_name]
|
33
|
+
process_id = event.payload[:process_id]
|
34
|
+
|
35
|
+
info_and_stdout(tags: [process_id]) do
|
36
|
+
"GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads} poll_interval=#{poll_interval}."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def scheduler_shutdown_start(event)
|
41
|
+
process_id = event.payload[:process_id]
|
42
|
+
|
43
|
+
info_and_stdout(tags: [process_id]) do
|
44
|
+
"GoodJob shutting down scheduler..."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def scheduler_shutdown(event)
|
49
|
+
process_id = event.payload[:process_id]
|
50
|
+
|
51
|
+
info_and_stdout(tags: [process_id]) do
|
52
|
+
"GoodJob scheduler is shut down."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def scheduler_restart_pools(event)
|
57
|
+
process_id = event.payload[:process_id]
|
58
|
+
|
59
|
+
info_and_stdout(tags: [process_id]) do
|
60
|
+
"GoodJob scheduler has restarted."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def cleanup_preserved_jobs(event)
|
65
|
+
timestamp = event.payload[:timestamp]
|
66
|
+
deleted_records_count = event.payload[:deleted_records_count]
|
67
|
+
|
68
|
+
info_and_stdout do
|
69
|
+
"GoodJob deleted #{deleted_records_count} preserved #{'job'.pluralize(deleted_records_count)} finished before #{timestamp}."
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def logger
|
76
|
+
GoodJob.logger
|
77
|
+
end
|
78
|
+
|
79
|
+
%w(info debug warn error fatal unknown).each do |level|
|
80
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
81
|
+
def #{level}(progname = nil, tags: [], &block)
|
82
|
+
return unless logger
|
83
|
+
|
84
|
+
if logger.respond_to?(:tagged)
|
85
|
+
tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
|
86
|
+
logger.tagged(*tags.compact) do
|
87
|
+
logger.#{level}(progname, &block)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
logger.#{level}(progname, &block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
METHOD
|
94
|
+
end
|
95
|
+
|
96
|
+
def info_and_stdout(progname = nil, tags: [], &block)
|
97
|
+
unless ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
|
98
|
+
tags_string = (['GoodJob'] + tags).map { |t| "[#{t}]" }.join(' ')
|
99
|
+
stdout_message = "#{tags_string}#{yield}"
|
100
|
+
$stdout.puts stdout_message
|
101
|
+
end
|
102
|
+
|
103
|
+
info(progname, tags: [], &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
def thread_name
|
107
|
+
Thread.current.name || Thread.current.object_id
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/good_job/performer.rb
CHANGED
data/lib/good_job/railtie.rb
CHANGED
data/lib/good_job/scheduler.rb
CHANGED
@@ -20,32 +20,31 @@ module GoodJob
|
|
20
20
|
fallback_policy: :discard,
|
21
21
|
}.freeze
|
22
22
|
|
23
|
+
cattr_reader :instances, default: [], instance_reader: false
|
24
|
+
|
23
25
|
def initialize(performer, timer_options: {}, pool_options: {})
|
24
26
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
25
27
|
|
28
|
+
self.class.instances << self
|
29
|
+
|
26
30
|
@performer = performer
|
27
|
-
@
|
28
|
-
@
|
29
|
-
create_thread
|
30
|
-
end
|
31
|
-
@timer.add_observer(self, :timer_observer)
|
32
|
-
@timer.execute
|
33
|
-
end
|
31
|
+
@pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
|
32
|
+
@timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
|
34
33
|
|
35
|
-
|
34
|
+
create_pools
|
36
35
|
end
|
37
36
|
|
38
37
|
def shutdown(wait: true)
|
39
38
|
@_shutdown = true
|
40
39
|
|
41
|
-
ActiveSupport::Notifications.instrument("
|
42
|
-
ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait }) do
|
43
|
-
if @timer
|
40
|
+
ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
|
41
|
+
ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait, process_id: process_id }) do
|
42
|
+
if @timer&.running?
|
44
43
|
@timer.shutdown
|
45
44
|
@timer.wait_for_termination if wait
|
46
45
|
end
|
47
46
|
|
48
|
-
if @pool
|
47
|
+
if @pool&.running?
|
49
48
|
@pool.shutdown
|
50
49
|
@pool.wait_for_termination if wait
|
51
50
|
end
|
@@ -56,6 +55,13 @@ module GoodJob
|
|
56
55
|
@_shutdown
|
57
56
|
end
|
58
57
|
|
58
|
+
def restart(wait: true)
|
59
|
+
ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
|
60
|
+
shutdown(wait: wait) unless shutdown?
|
61
|
+
create_pools
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
59
65
|
def create_thread
|
60
66
|
return false unless @pool.ready_worker_count.positive?
|
61
67
|
|
@@ -69,10 +75,12 @@ module GoodJob
|
|
69
75
|
end
|
70
76
|
|
71
77
|
def timer_observer(time, executed_task, thread_error)
|
78
|
+
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
72
79
|
ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
|
73
80
|
end
|
74
81
|
|
75
82
|
def task_observer(time, output, thread_error)
|
83
|
+
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
76
84
|
ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
|
77
85
|
create_thread if output
|
78
86
|
end
|
@@ -88,5 +96,26 @@ module GoodJob
|
|
88
96
|
end
|
89
97
|
end
|
90
98
|
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def create_pools
|
103
|
+
ActiveSupport::Notifications.instrument("scheduler_create_pools.good_job", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval], process_id: process_id }) do
|
104
|
+
@pool = ThreadPoolExecutor.new(@pool_options)
|
105
|
+
next unless @timer_options[:execution_interval].positive?
|
106
|
+
|
107
|
+
@timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
|
108
|
+
@timer.add_observer(self, :timer_observer)
|
109
|
+
@timer.execute
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def process_id
|
114
|
+
Process.pid
|
115
|
+
end
|
116
|
+
|
117
|
+
def thread_name
|
118
|
+
Thread.current.name || Thread.current.object_id
|
119
|
+
end
|
91
120
|
end
|
92
121
|
end
|
data/lib/good_job/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: dotenv
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: foreman
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,6 +164,20 @@ dependencies:
|
|
150
164
|
- - ">="
|
151
165
|
- !ruby/object:Gem::Version
|
152
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: puma
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
153
181
|
- !ruby/object:Gem::Dependency
|
154
182
|
name: rspec-rails
|
155
183
|
requirement: !ruby/object:Gem::Requirement
|
@@ -243,7 +271,7 @@ files:
|
|
243
271
|
- lib/good_job/cli.rb
|
244
272
|
- lib/good_job/job.rb
|
245
273
|
- lib/good_job/lockable.rb
|
246
|
-
- lib/good_job/
|
274
|
+
- lib/good_job/log_subscriber.rb
|
247
275
|
- lib/good_job/performer.rb
|
248
276
|
- lib/good_job/pg_locks.rb
|
249
277
|
- lib/good_job/railtie.rb
|
@@ -258,7 +286,7 @@ metadata:
|
|
258
286
|
documentation_uri: https://rdoc.info/github/bensheldon/good_job
|
259
287
|
homepage_uri: https://github.com/bensheldon/good_job
|
260
288
|
source_code_uri: https://github.com/bensheldon/good_job
|
261
|
-
post_install_message:
|
289
|
+
post_install_message:
|
262
290
|
rdoc_options:
|
263
291
|
- "--title"
|
264
292
|
- GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
@@ -281,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
281
309
|
version: '0'
|
282
310
|
requirements: []
|
283
311
|
rubygems_version: 3.0.3
|
284
|
-
signing_key:
|
312
|
+
signing_key:
|
285
313
|
specification_version: 4
|
286
314
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
287
315
|
test_files: []
|
data/lib/good_job/logging.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
module GoodJob
|
2
|
-
module Logging
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
included do
|
6
|
-
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
7
|
-
|
8
|
-
def self.tag_logger(*tags)
|
9
|
-
if logger.respond_to?(:tagged)
|
10
|
-
tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
|
11
|
-
logger.tagged(*tags) { yield }
|
12
|
-
else
|
13
|
-
yield
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
class LogSubscriber < ActiveSupport::LogSubscriber
|
19
|
-
def create(event)
|
20
|
-
good_job = event.payload[:good_job]
|
21
|
-
|
22
|
-
info do
|
23
|
-
"Created GoodJob resource with id #{good_job.id}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def timer_task_finished(event)
|
28
|
-
exception = event.payload[:error]
|
29
|
-
return unless exception
|
30
|
-
|
31
|
-
error do
|
32
|
-
"ERROR: #{exception}\n #{exception.backtrace}"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def job_finished(event)
|
37
|
-
exception = event.payload[:error]
|
38
|
-
return unless exception
|
39
|
-
|
40
|
-
error do
|
41
|
-
"ERROR: #{exception}\n #{exception.backtrace}"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def scheduler_start_shutdown(_event)
|
46
|
-
info do
|
47
|
-
"Shutting down scheduler..."
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def scheduler_shutdown(_event)
|
52
|
-
info do
|
53
|
-
"Scheduler is shut down."
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def logger
|
60
|
-
GoodJob.logger
|
61
|
-
end
|
62
|
-
|
63
|
-
def thread_name
|
64
|
-
Thread.current.name || Thread.current.object_id
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
GoodJob::Logging::LogSubscriber.attach_to :good_job
|