good_job 0.9.0 → 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 +99 -3
- data/README.md +175 -46
- data/lib/active_job/queue_adapters/good_job_adapter.rb +20 -3
- data/lib/generators/good_job/install_generator.rb +24 -0
- data/lib/generators/good_job/templates/migration.rb.erb +20 -0
- data/lib/good_job.rb +4 -2
- data/lib/good_job/adapter.rb +26 -8
- data/lib/good_job/cli.rb +18 -21
- data/lib/good_job/job.rb +30 -25
- 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 +58 -16
- data/lib/good_job/version.rb +1 -1
- metadata +36 -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,21 +1,117 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
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)
|
23
|
+
|
24
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.2...v1.0.3)
|
25
|
+
|
26
|
+
**Fixed bugs:**
|
27
|
+
|
28
|
+
- Preserve GoodJob::Jobs when a StandardError is raised [\#60](https://github.com/bensheldon/good_job/issues/60)
|
29
|
+
|
30
|
+
**Closed issues:**
|
31
|
+
|
32
|
+
- Have an initial setup generator [\#6](https://github.com/bensheldon/good_job/issues/6)
|
33
|
+
|
34
|
+
**Merged pull requests:**
|
35
|
+
|
36
|
+
- Re-perform a job if a StandardError bubbles up; better document job reliability [\#62](https://github.com/bensheldon/good_job/pull/62) ([bensheldon](https://github.com/bensheldon))
|
37
|
+
- Update the setup documentation to use correct bin setup command [\#61](https://github.com/bensheldon/good_job/pull/61) ([jm96441n](https://github.com/jm96441n))
|
38
|
+
|
39
|
+
## [v1.0.2](https://github.com/bensheldon/good_job/tree/v1.0.2) (2020-07-25)
|
40
|
+
|
41
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.1...v1.0.2)
|
42
|
+
|
43
|
+
**Fixed bugs:**
|
44
|
+
|
45
|
+
- Fix counting of available execution threads [\#58](https://github.com/bensheldon/good_job/pull/58) ([bensheldon](https://github.com/bensheldon))
|
46
|
+
|
47
|
+
**Closed issues:**
|
48
|
+
|
49
|
+
- repeating/recurring jobs [\#53](https://github.com/bensheldon/good_job/issues/53)
|
50
|
+
|
51
|
+
**Merged pull requests:**
|
52
|
+
|
53
|
+
- Add migration generator [\#56](https://github.com/bensheldon/good_job/pull/56) ([thedanbob](https://github.com/thedanbob))
|
54
|
+
- Fix migration script in readme [\#55](https://github.com/bensheldon/good_job/pull/55) ([thedanbob](https://github.com/thedanbob))
|
55
|
+
|
56
|
+
## [v1.0.1](https://github.com/bensheldon/good_job/tree/v1.0.1) (2020-07-22)
|
57
|
+
|
58
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.0...v1.0.1)
|
59
|
+
|
60
|
+
**Merged pull requests:**
|
61
|
+
|
62
|
+
- Change threadpool idletime default to 60 seconds from 0 [\#49](https://github.com/bensheldon/good_job/pull/49) ([bensheldon](https://github.com/bensheldon))
|
63
|
+
|
64
|
+
## [v1.0.0](https://github.com/bensheldon/good_job/tree/v1.0.0) (2020-07-20)
|
65
|
+
|
66
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.9.0...v1.0.0)
|
67
|
+
|
3
68
|
## [v0.9.0](https://github.com/bensheldon/good_job/tree/v0.9.0) (2020-07-20)
|
4
69
|
|
5
|
-
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.
|
70
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.2...v0.9.0)
|
71
|
+
|
72
|
+
**Merged pull requests:**
|
73
|
+
|
74
|
+
- Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
|
75
|
+
|
76
|
+
## [v0.8.2](https://github.com/bensheldon/good_job/tree/v0.8.2) (2020-07-18)
|
77
|
+
|
78
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.1...v0.8.2)
|
6
79
|
|
7
80
|
**Closed issues:**
|
8
81
|
|
9
|
-
- Always store a default priority \(0\) and scheduled\_at\(Time.current\) [\#30](https://github.com/bensheldon/good_job/issues/30)
|
10
82
|
- Add a job timeout configuration to time out jobs that have run too long [\#19](https://github.com/bensheldon/good_job/issues/19)
|
11
83
|
|
12
84
|
**Merged pull requests:**
|
13
85
|
|
14
|
-
- Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
|
15
86
|
- Run Github Action tests on PRs from forks [\#44](https://github.com/bensheldon/good_job/pull/44) ([bensheldon](https://github.com/bensheldon))
|
16
87
|
- Fix Rubygems homepage URL [\#43](https://github.com/bensheldon/good_job/pull/43) ([joshmn](https://github.com/joshmn))
|
88
|
+
|
89
|
+
## [v0.8.1](https://github.com/bensheldon/good_job/tree/v0.8.1) (2020-07-18)
|
90
|
+
|
91
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.0...v0.8.1)
|
92
|
+
|
93
|
+
**Merged pull requests:**
|
94
|
+
|
17
95
|
- Move where\(scheduled\_at: Time.current\) into dynamic part of GoodJob::Job::Performer [\#42](https://github.com/bensheldon/good_job/pull/42) ([bensheldon](https://github.com/bensheldon))
|
96
|
+
|
97
|
+
## [v0.8.0](https://github.com/bensheldon/good_job/tree/v0.8.0) (2020-07-17)
|
98
|
+
|
99
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.7.0...v0.8.0)
|
100
|
+
|
101
|
+
**Merged pull requests:**
|
102
|
+
|
18
103
|
- Replace Adapter inline boolean kwarg with execution\_mode instead [\#41](https://github.com/bensheldon/good_job/pull/41) ([bensheldon](https://github.com/bensheldon))
|
104
|
+
|
105
|
+
## [v0.7.0](https://github.com/bensheldon/good_job/tree/v0.7.0) (2020-07-16)
|
106
|
+
|
107
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v0.7.0)
|
108
|
+
|
109
|
+
**Closed issues:**
|
110
|
+
|
111
|
+
- Always store a default priority \(0\) and scheduled\_at\(Time.current\) [\#30](https://github.com/bensheldon/good_job/issues/30)
|
112
|
+
|
113
|
+
**Merged pull requests:**
|
114
|
+
|
19
115
|
- Add more examples to Readme [\#39](https://github.com/bensheldon/good_job/pull/39) ([bensheldon](https://github.com/bensheldon))
|
20
116
|
- Add additional Rubocops and lint [\#38](https://github.com/bensheldon/good_job/pull/38) ([bensheldon](https://github.com/bensheldon))
|
21
117
|
- Always store a default queue\_name, priority and scheduled\_at; index by queue\_name and scheduled\_at [\#37](https://github.com/bensheldon/good_job/pull/37) ([bensheldon](https://github.com/bensheldon))
|
data/README.md
CHANGED
@@ -9,6 +9,8 @@ GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
|
|
9
9
|
- **Backed by Postgres.** Relies upon Postgres integrity and session-level Advisory Locks to provide run-once safety and stay within the limits of `schema.rb`.
|
10
10
|
- **For most workloads.** Targets full-stack teams, economy-minded solo developers, and applications that enqueue less than 1-million jobs/day.
|
11
11
|
|
12
|
+
For more of the story of GoodJob, read the [introductory blog post](https://island94.org/2020/07/introducing-goodjob-1-0).
|
13
|
+
|
12
14
|
## Installation
|
13
15
|
|
14
16
|
Add this line to your application's Gemfile:
|
@@ -25,35 +27,11 @@ $ bundle install
|
|
25
27
|
## Usage
|
26
28
|
|
27
29
|
1. Create a database migration:
|
28
|
-
|
29
|
-
|
30
|
+
|
31
|
+
```bash
|
32
|
+
$ bin/rails g good_job:install
|
30
33
|
```
|
31
34
|
|
32
|
-
Add to the newly created migration file:
|
33
|
-
|
34
|
-
```ruby
|
35
|
-
class CreateGoodJobs < ActiveRecord::Migration[6.0]
|
36
|
-
def change
|
37
|
-
enable_extension 'pgcrypto'
|
38
|
-
|
39
|
-
create_table :good_jobs, id: :uuid do |t|
|
40
|
-
t.timestamps
|
41
|
-
|
42
|
-
t.text :queue_name
|
43
|
-
t.integer :priority
|
44
|
-
t.jsonb :serialized_params
|
45
|
-
t.timestamp :scheduled_at
|
46
|
-
t.timestamp :performed_at
|
47
|
-
t.timestamp :finished_at
|
48
|
-
t.text :error
|
49
|
-
|
50
|
-
add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)"
|
51
|
-
add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
```
|
56
|
-
|
57
35
|
Run the migration:
|
58
36
|
|
59
37
|
```bash
|
@@ -61,7 +39,8 @@ $ bundle install
|
|
61
39
|
```
|
62
40
|
|
63
41
|
1. Configure the ActiveJob adapter:
|
64
|
-
|
42
|
+
|
43
|
+
```ruby
|
65
44
|
# config/application.rb
|
66
45
|
config.active_job.queue_adapter = :good_job
|
67
46
|
```
|
@@ -80,16 +59,19 @@ $ bundle install
|
|
80
59
|
```
|
81
60
|
|
82
61
|
1. Queue your job 🎉:
|
62
|
+
|
83
63
|
```ruby
|
84
64
|
YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later
|
85
65
|
```
|
86
66
|
|
87
67
|
1. In production, the scheduler is designed to run in its own process:
|
68
|
+
|
88
69
|
```bash
|
89
70
|
$ bundle exec good_job
|
90
71
|
```
|
91
72
|
|
92
73
|
Configuration options available with `help`:
|
74
|
+
|
93
75
|
```bash
|
94
76
|
$ bundle exec good_job help start
|
95
77
|
|
@@ -102,26 +84,120 @@ $ bundle install
|
|
102
84
|
# [--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
|
103
85
|
```
|
104
86
|
|
105
|
-
###
|
87
|
+
### Error handling, retries, and reliability
|
88
|
+
|
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.
|
90
|
+
|
91
|
+
#### Error handling
|
92
|
+
|
93
|
+
By default, if a job raises an error while it is being performed, _and it bubbles up to the GoodJob backend_, GoodJob will be immediately re-perform the job until it finishes successfully.
|
94
|
+
|
95
|
+
- `Exception`-type errors, such as a SIGINT, will always cause a job to be re-performed.
|
96
|
+
- `StandardError`-type errors, by default, will cause a job to be re-performed, though this is configurable:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# config/initializers/good_job.rb
|
100
|
+
GoodJob.reperform_jobs_on_standard_error = true # => default
|
101
|
+
```
|
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
|
+
|
112
|
+
### Retrying jobs
|
113
|
+
|
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:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class ApplicationJob < ActiveJob::Base
|
118
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
|
119
|
+
# ...
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
When specifying a limited number of retries, care must be taken to ensure that an error does not bubble up to the GoodJob backend because that will result in the job being re-performed:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class ApplicationJob < ActiveJob::Base
|
127
|
+
retry_on StandardError, attempts: 5 do |_job, _exception|
|
128
|
+
# Log error, etc.
|
129
|
+
# You must implement this block, otherwise,
|
130
|
+
# Active Job will re-raise the error.
|
131
|
+
# Do not re-raise the error, otherwise
|
132
|
+
# GoodJob will immediately re-perform the job.
|
133
|
+
end
|
134
|
+
# ...
|
135
|
+
end
|
136
|
+
```
|
106
137
|
|
107
|
-
|
138
|
+
GoodJob can be configured to allow omitting `retry_on`'s block argument and implicitly discard un-handled errors:
|
139
|
+
|
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:
|
108
148
|
|
109
149
|
```ruby
|
110
150
|
class ApplicationJob < ActiveJob::Base
|
111
|
-
# Retry errors an infinite number of times with exponential back-off
|
112
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
|
+
|
163
|
+
|
164
|
+
ActiveJob's `discard_on` functionality is supported too.
|
165
|
+
|
166
|
+
#### ActionMailer retries
|
113
167
|
|
114
|
-
|
168
|
+
Using a Mailer's `#deliver_later` will enqueue an instance of `ActionMailer::DeliveryJob` which inherits from `ActiveJob::Base` rather than your applications `ApplicationJob`. You can use an initializer to configure retries on `ActionMailer::DeliveryJob`:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# config/initializers/good_job.rb
|
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
|
181
|
+
```
|
182
|
+
|
183
|
+
#### Timeouts
|
184
|
+
|
185
|
+
Job timeouts can be configured with an `around_perform`:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class ApplicationJob < ActiveJob::Base
|
115
189
|
JobTimeoutError = Class.new(StandardError)
|
190
|
+
|
116
191
|
around_perform do |_job, block|
|
192
|
+
# Timeout jobs after 10 minutes
|
117
193
|
Timeout.timeout(10.minutes, JobTimeoutError) do
|
118
194
|
block.call
|
119
195
|
end
|
120
196
|
end
|
121
197
|
end
|
122
198
|
```
|
123
|
-
|
124
|
-
### Configuring
|
199
|
+
|
200
|
+
### Configuring job execution threads
|
125
201
|
|
126
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:
|
127
203
|
|
@@ -132,6 +208,51 @@ GoodJob executes enqueued jobs using threads. There is a lot than can be said ab
|
|
132
208
|
3. `$ RAILS_MAX_THREADS=4 bundle exec good_job`
|
133
209
|
4. Implicitly via Rails's database connection pool size (`ActiveRecord::Base.connection_pool.size`)
|
134
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
|
+
|
135
256
|
### Migrating to GoodJob from a different ActiveJob backend
|
136
257
|
|
137
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.
|
@@ -147,7 +268,8 @@ If your application is already using an ActiveJob backend, you will need to inst
|
|
147
268
|
```
|
148
269
|
|
149
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:
|
150
|
-
|
271
|
+
|
272
|
+
```procfile
|
151
273
|
# Procfile
|
152
274
|
# ...
|
153
275
|
worker: bundle exec que ./config/environment.rb & bundle exec good_job & wait -n
|
@@ -173,15 +295,24 @@ It is also necessary to delete these preserved jobs from the database after a ce
|
|
173
295
|
- For example, in a Rake task:
|
174
296
|
|
175
297
|
```ruby
|
176
|
-
|
298
|
+
GoodJob::Job.finished(1.day.ago).delete_all
|
177
299
|
```
|
300
|
+
|
178
301
|
- For example, using the `good_job` command-line utility:
|
179
302
|
|
180
303
|
```bash
|
181
304
|
$ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
|
182
305
|
```
|
183
306
|
|
184
|
-
##
|
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
|
185
316
|
|
186
317
|
To run tests:
|
187
318
|
|
@@ -190,7 +321,7 @@ To run tests:
|
|
190
321
|
$ git clone git@github.com:bensheldon/good_job.git
|
191
322
|
|
192
323
|
# Set up the local environment
|
193
|
-
$ bin/
|
324
|
+
$ bin/setup
|
194
325
|
|
195
326
|
# Run the tests
|
196
327
|
$ bin/rspec
|
@@ -204,7 +335,6 @@ $ bundle exec appraisal
|
|
204
335
|
|
205
336
|
# Run tests
|
206
337
|
$ bundle exec appraisal bin/rspec
|
207
|
-
|
208
338
|
```
|
209
339
|
|
210
340
|
For developing locally within another Ruby on Rails project:
|
@@ -219,24 +349,23 @@ $ bundle install
|
|
219
349
|
# => Using good_job 0.1.0 from https://github.com/bensheldon/good_job.git (at /Users/You/Projects/good_job@dc57fb0)
|
220
350
|
```
|
221
351
|
|
222
|
-
|
352
|
+
### Releasing
|
223
353
|
|
224
|
-
Package maintainers can release this gem
|
354
|
+
Package maintainers can release this gem by running:
|
225
355
|
|
226
356
|
```bash
|
227
357
|
# Sign into rubygems
|
228
358
|
$ gem signin
|
229
359
|
|
360
|
+
# Add a .env file with the following:
|
361
|
+
# CHANGELOG_GITHUB_TOKEN= # Github Personal Access Token
|
362
|
+
|
230
363
|
# Update version number, changelog, and create git commit:
|
231
|
-
$ bundle exec rake
|
364
|
+
$ bundle exec rake release[minor] # major,minor,patch
|
232
365
|
|
233
366
|
# ..and follow subsequent directions.
|
234
367
|
```
|
235
368
|
|
236
|
-
## Contributing
|
237
|
-
|
238
|
-
Contribution directions go here.
|
239
|
-
|
240
369
|
## License
|
241
370
|
|
242
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
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module GoodJob
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
class << self
|
9
|
+
delegate :next_migration_number, to: ActiveRecord::Generators::Base
|
10
|
+
end
|
11
|
+
|
12
|
+
source_paths << File.join(File.dirname(__FILE__), "templates")
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template 'migration.rb.erb', 'db/migrate/create_good_jobs.rb', migration_version: migration_version
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def migration_version
|
21
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
enable_extension 'pgcrypto'
|
4
|
+
|
5
|
+
create_table :good_jobs, id: :uuid do |t|
|
6
|
+
t.text :queue_name
|
7
|
+
t.integer :priority
|
8
|
+
t.jsonb :serialized_params
|
9
|
+
t.timestamp :scheduled_at
|
10
|
+
t.timestamp :performed_at
|
11
|
+
t.timestamp :finished_at
|
12
|
+
t.text :error
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)"
|
18
|
+
add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)"
|
19
|
+
end
|
20
|
+
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,8 +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
|
18
|
+
mattr_accessor :on_thread_error, default: nil
|
17
19
|
|
18
20
|
ActiveSupport.run_load_hooks(:good_job, self)
|
19
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,29 +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
|
-
|
43
|
-
performer_method = if GoodJob.preserve_job_records
|
44
|
-
:perform_with_advisory_lock_and_preserve_job_records
|
45
|
-
else
|
46
|
-
:perform_with_advisory_lock_and_destroy_job_records
|
47
|
-
end
|
48
|
-
job_performer = GoodJob::Performer.new(job_query, performer_method)
|
49
|
-
|
50
|
-
$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)
|
51
41
|
|
52
42
|
timer_options = {}
|
53
43
|
timer_options[:execution_interval] = poll_interval if poll_interval.positive?
|
@@ -68,24 +58,31 @@ module GoodJob
|
|
68
58
|
break if @stop_good_job_executable || scheduler.shutdown?
|
69
59
|
end
|
70
60
|
|
71
|
-
$stdout.puts "\nFinishing GoodJob's current jobs before exiting..."
|
72
61
|
scheduler.shutdown
|
73
|
-
$stdout.puts "GoodJob's jobs finished, exiting..."
|
74
62
|
end
|
75
63
|
|
64
|
+
default_task :start
|
65
|
+
|
76
66
|
desc :cleanup_preserved_jobs, "Delete preserved job records"
|
77
67
|
method_option :before_seconds_ago,
|
78
68
|
type: :numeric,
|
79
69
|
default: 24 * 60 * 60,
|
80
70
|
desc: "Delete records finished more than this many seconds ago"
|
81
71
|
def cleanup_preserved_jobs
|
82
|
-
|
72
|
+
set_up_application!
|
83
73
|
|
84
74
|
timestamp = Time.current - options[:before_seconds_ago]
|
85
|
-
|
86
|
-
|
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
|
87
80
|
end
|
88
81
|
|
89
|
-
|
82
|
+
no_commands do
|
83
|
+
def set_up_application!
|
84
|
+
require RAILS_ENVIRONMENT_RB
|
85
|
+
end
|
86
|
+
end
|
90
87
|
end
|
91
88
|
end
|
data/lib/good_job/job.rb
CHANGED
@@ -18,32 +18,28 @@ 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
|
-
def self.perform_with_advisory_lock
|
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
|
|
33
|
-
result, error = good_job.perform
|
37
|
+
result, error = good_job.perform
|
34
38
|
end
|
35
39
|
|
36
40
|
[good_job, result, error] if good_job
|
37
41
|
end
|
38
42
|
|
39
|
-
def self.perform_with_advisory_lock_and_preserve_job_records
|
40
|
-
perform_with_advisory_lock(destroy_after: false)
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.perform_with_advisory_lock_and_destroy_job_records
|
44
|
-
perform_with_advisory_lock(destroy_after: true)
|
45
|
-
end
|
46
|
-
|
47
43
|
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
48
44
|
good_job = nil
|
49
45
|
ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
|
@@ -64,40 +60,49 @@ module GoodJob
|
|
64
60
|
good_job
|
65
61
|
end
|
66
62
|
|
67
|
-
def perform(destroy_after:
|
63
|
+
def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
|
68
64
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
69
65
|
|
70
66
|
result = nil
|
67
|
+
rescued_error = nil
|
71
68
|
error = nil
|
72
69
|
|
73
70
|
ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
|
74
71
|
self.performed_at = Time.current
|
75
72
|
save! unless destroy_after
|
76
73
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
74
|
+
params = serialized_params.merge(
|
75
|
+
"provider_job_id" => id
|
76
|
+
)
|
77
|
+
|
78
|
+
begin
|
79
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
|
82
80
|
result = ActiveJob::Base.execute(params)
|
83
|
-
rescue StandardError => e
|
84
|
-
error = e
|
85
81
|
end
|
82
|
+
rescue StandardError => e
|
83
|
+
rescued_error = e
|
86
84
|
end
|
87
85
|
|
88
|
-
if
|
86
|
+
if rescued_error
|
87
|
+
error = rescued_error
|
88
|
+
elsif result.is_a?(Exception)
|
89
89
|
error = result
|
90
90
|
result = nil
|
91
91
|
end
|
92
92
|
|
93
93
|
error_message = "#{error.class}: #{error.message}" if error
|
94
94
|
self.error = error_message
|
95
|
-
self.finished_at = Time.current
|
96
95
|
|
97
|
-
if
|
98
|
-
destroy!
|
99
|
-
else
|
96
|
+
if rescued_error && reperform_on_standard_error
|
100
97
|
save!
|
98
|
+
else
|
99
|
+
self.finished_at = Time.current
|
100
|
+
|
101
|
+
if destroy_after
|
102
|
+
destroy!
|
103
|
+
else
|
104
|
+
save!
|
105
|
+
end
|
101
106
|
end
|
102
107
|
|
103
108
|
[result, error]
|
@@ -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
@@ -15,38 +15,36 @@ module GoodJob
|
|
15
15
|
min_threads: 0,
|
16
16
|
max_threads: Concurrent.processor_count,
|
17
17
|
auto_terminate: true,
|
18
|
-
idletime:
|
19
|
-
max_queue:
|
20
|
-
fallback_policy: :
|
18
|
+
idletime: 60,
|
19
|
+
max_queue: -1,
|
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
|
-
idle_threads = @pool.max_length - @pool.length
|
30
|
-
create_thread if idle_threads.positive?
|
31
|
-
end
|
32
|
-
@timer.add_observer(self, :timer_observer)
|
33
|
-
@timer.execute
|
34
|
-
end
|
31
|
+
@pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
|
32
|
+
@timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
|
35
33
|
|
36
|
-
|
34
|
+
create_pools
|
37
35
|
end
|
38
36
|
|
39
37
|
def shutdown(wait: true)
|
40
38
|
@_shutdown = true
|
41
39
|
|
42
|
-
ActiveSupport::Notifications.instrument("
|
43
|
-
ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait }) do
|
44
|
-
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?
|
45
43
|
@timer.shutdown
|
46
44
|
@timer.wait_for_termination if wait
|
47
45
|
end
|
48
46
|
|
49
|
-
if @pool
|
47
|
+
if @pool&.running?
|
50
48
|
@pool.shutdown
|
51
49
|
@pool.wait_for_termination if wait
|
52
50
|
end
|
@@ -57,7 +55,16 @@ module GoodJob
|
|
57
55
|
@_shutdown
|
58
56
|
end
|
59
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
|
+
|
60
65
|
def create_thread
|
66
|
+
return false unless @pool.ready_worker_count.positive?
|
67
|
+
|
61
68
|
future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
|
62
69
|
output = nil
|
63
70
|
Rails.application.executor.wrap { output = performer.next }
|
@@ -68,12 +75,47 @@ module GoodJob
|
|
68
75
|
end
|
69
76
|
|
70
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)
|
71
79
|
ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
|
72
80
|
end
|
73
81
|
|
74
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)
|
75
84
|
ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
|
76
85
|
create_thread if output
|
77
86
|
end
|
87
|
+
|
88
|
+
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
89
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
|
90
|
+
def ready_worker_count
|
91
|
+
synchronize do
|
92
|
+
workers_still_to_be_created = @max_length - @pool.length
|
93
|
+
workers_created_but_waiting = @ready.length
|
94
|
+
|
95
|
+
workers_still_to_be_created + workers_created_but_waiting
|
96
|
+
end
|
97
|
+
end
|
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
|
78
120
|
end
|
79
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:
|
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
|
@@ -236,12 +264,14 @@ files:
|
|
236
264
|
- README.md
|
237
265
|
- exe/good_job
|
238
266
|
- lib/active_job/queue_adapters/good_job_adapter.rb
|
267
|
+
- lib/generators/good_job/install_generator.rb
|
268
|
+
- lib/generators/good_job/templates/migration.rb.erb
|
239
269
|
- lib/good_job.rb
|
240
270
|
- lib/good_job/adapter.rb
|
241
271
|
- lib/good_job/cli.rb
|
242
272
|
- lib/good_job/job.rb
|
243
273
|
- lib/good_job/lockable.rb
|
244
|
-
- lib/good_job/
|
274
|
+
- lib/good_job/log_subscriber.rb
|
245
275
|
- lib/good_job/performer.rb
|
246
276
|
- lib/good_job/pg_locks.rb
|
247
277
|
- lib/good_job/railtie.rb
|
@@ -256,7 +286,7 @@ metadata:
|
|
256
286
|
documentation_uri: https://rdoc.info/github/bensheldon/good_job
|
257
287
|
homepage_uri: https://github.com/bensheldon/good_job
|
258
288
|
source_code_uri: https://github.com/bensheldon/good_job
|
259
|
-
post_install_message:
|
289
|
+
post_install_message:
|
260
290
|
rdoc_options:
|
261
291
|
- "--title"
|
262
292
|
- GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
@@ -279,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
279
309
|
version: '0'
|
280
310
|
requirements: []
|
281
311
|
rubygems_version: 3.0.3
|
282
|
-
signing_key:
|
312
|
+
signing_key:
|
283
313
|
specification_version: 4
|
284
314
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
285
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
|