good_job 1.0.3 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|