good_job 1.4.0 → 1.7.1
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 +78 -13
- data/README.md +55 -12
- data/exe/good_job +1 -0
- data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -3
- data/lib/good_job.rb +7 -8
- data/lib/good_job/adapter.rb +25 -19
- data/lib/good_job/cli.rb +19 -1
- data/lib/good_job/configuration.rb +57 -12
- data/lib/good_job/daemon.rb +59 -0
- data/lib/good_job/job.rb +23 -0
- data/lib/good_job/job_performer.rb +73 -0
- data/lib/good_job/lockable.rb +2 -2
- data/lib/good_job/notifier.rb +1 -0
- data/lib/good_job/poller.rb +3 -0
- data/lib/good_job/railtie.rb +10 -2
- data/lib/good_job/scheduler.rb +95 -26
- data/lib/good_job/version.rb +1 -1
- metadata +5 -4
- data/lib/good_job/performer.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86268c6767ba44783707ed768fe726b7a53592e46468ed49bd38d427ff4919af
|
4
|
+
data.tar.gz: 5fa72a4aaec59a31ba437eac31f9a6785cdb548c6694bea332434baf58aaad98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c63baa745a861101b1a65c7a2dd7f4ec603a50714d5952a7ad942f30b41543643e6b7e1f19b1373f280668f212c6e173c4aa90d7d3a0770a92c1e159ae30215b
|
7
|
+
data.tar.gz: 7bf564c4cfc0804f1a32995214195736e9cbd8d8f7a5b45d3ffe7efecf3d43126d412594855d50c9637c646e82838112a4869a4732fa0fe5ba1542b558facd0f
|
data/CHANGELOG.md
CHANGED
@@ -1,17 +1,88 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [v1.
|
3
|
+
## [v1.7.1](https://github.com/bensheldon/good_job/tree/v1.7.1) (2021-01-27)
|
4
4
|
|
5
|
-
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.7.0...v1.7.1)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- Unexpected behavior with max\_threads = 1 [\#208](https://github.com/bensheldon/good_job/issues/208)
|
10
|
+
|
11
|
+
**Merged pull requests:**
|
12
|
+
|
13
|
+
- Scheduler should always push a new task on completion of previous task, regardless of available thread calculation [\#209](https://github.com/bensheldon/good_job/pull/209) ([bensheldon](https://github.com/bensheldon))
|
14
|
+
- Fix equality typo in development.rb of test\_app [\#207](https://github.com/bensheldon/good_job/pull/207) ([reczy](https://github.com/reczy))
|
15
|
+
|
16
|
+
## [v1.7.0](https://github.com/bensheldon/good_job/tree/v1.7.0) (2021-01-25)
|
17
|
+
|
18
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.6.0...v1.7.0)
|
19
|
+
|
20
|
+
**Merged pull requests:**
|
21
|
+
|
22
|
+
- Cache scheduled jobs in memory so they can be executed without polling [\#205](https://github.com/bensheldon/good_job/pull/205) ([bensheldon](https://github.com/bensheldon))
|
23
|
+
|
24
|
+
## [v1.6.0](https://github.com/bensheldon/good_job/tree/v1.6.0) (2021-01-22)
|
25
|
+
|
26
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.5.0...v1.6.0)
|
6
27
|
|
7
28
|
**Implemented enhancements:**
|
8
29
|
|
9
|
-
-
|
30
|
+
- Running as a daemon [\#88](https://github.com/bensheldon/good_job/issues/88)
|
31
|
+
- Add daemonize option to CLI [\#202](https://github.com/bensheldon/good_job/pull/202) ([bensheldon](https://github.com/bensheldon))
|
32
|
+
|
33
|
+
**Closed issues:**
|
34
|
+
|
35
|
+
- Rails 6.1 & async - `queue\_parser': undefined method `first' for "\*":String \(NoMethodError\) [\#195](https://github.com/bensheldon/good_job/issues/195)
|
36
|
+
|
37
|
+
**Merged pull requests:**
|
38
|
+
|
39
|
+
- Add scripts directory for benchmarking and dev tasks [\#204](https://github.com/bensheldon/good_job/pull/204) ([bensheldon](https://github.com/bensheldon))
|
40
|
+
- Fix YARD attr\_ declarations for documentation [\#203](https://github.com/bensheldon/good_job/pull/203) ([bensheldon](https://github.com/bensheldon))
|
41
|
+
- Remove Appraisal gemfile locks [\#201](https://github.com/bensheldon/good_job/pull/201) ([bensheldon](https://github.com/bensheldon))
|
42
|
+
|
43
|
+
## [v1.5.0](https://github.com/bensheldon/good_job/tree/v1.5.0) (2021-01-18)
|
44
|
+
|
45
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.4.1...v1.5.0)
|
46
|
+
|
47
|
+
**Implemented enhancements:**
|
48
|
+
|
49
|
+
- Create Web UI Dashboard [\#50](https://github.com/bensheldon/good_job/issues/50)
|
10
50
|
|
11
51
|
**Closed issues:**
|
12
52
|
|
13
53
|
- JRuby Support [\#160](https://github.com/bensheldon/good_job/issues/160)
|
14
54
|
|
55
|
+
**Merged pull requests:**
|
56
|
+
|
57
|
+
- Update bundler version to 2.2.5 [\#200](https://github.com/bensheldon/good_job/pull/200) ([bensheldon](https://github.com/bensheldon))
|
58
|
+
- Configure GoodJob via `Rails.application.config` instead of recommending `GoodJob::Adapter.new` [\#199](https://github.com/bensheldon/good_job/pull/199) ([bensheldon](https://github.com/bensheldon))
|
59
|
+
- Update GH Test Matrix with minimum & latest JRuby version [\#197](https://github.com/bensheldon/good_job/pull/197) ([tedhexaflow](https://github.com/tedhexaflow))
|
60
|
+
- Fix JRuby version number [\#193](https://github.com/bensheldon/good_job/pull/193) ([tedhexaflow](https://github.com/tedhexaflow))
|
61
|
+
|
62
|
+
## [v1.4.1](https://github.com/bensheldon/good_job/tree/v1.4.1) (2021-01-09)
|
63
|
+
|
64
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.4.0...v1.4.1)
|
65
|
+
|
66
|
+
**Fixed bugs:**
|
67
|
+
|
68
|
+
- Do not add lib/generators to Zeitwerk autoloader [\#192](https://github.com/bensheldon/good_job/pull/192) ([bensheldon](https://github.com/bensheldon))
|
69
|
+
|
70
|
+
**Closed issues:**
|
71
|
+
|
72
|
+
- Issues with Heroku and Good Job [\#184](https://github.com/bensheldon/good_job/issues/184)
|
73
|
+
|
74
|
+
**Merged pull requests:**
|
75
|
+
|
76
|
+
- Add missing YARD docs and Dashboard screenshot [\#191](https://github.com/bensheldon/good_job/pull/191) ([bensheldon](https://github.com/bensheldon))
|
77
|
+
|
78
|
+
## [v1.4.0](https://github.com/bensheldon/good_job/tree/v1.4.0) (2020-12-31)
|
79
|
+
|
80
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.6...v1.4.0)
|
81
|
+
|
82
|
+
**Implemented enhancements:**
|
83
|
+
|
84
|
+
- Add JRuby support [\#167](https://github.com/bensheldon/good_job/pull/167) ([bensheldon](https://github.com/bensheldon))
|
85
|
+
|
15
86
|
## [v1.3.6](https://github.com/bensheldon/good_job/tree/v1.3.6) (2020-12-30)
|
16
87
|
|
17
88
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.5...v1.3.6)
|
@@ -43,6 +114,7 @@
|
|
43
114
|
|
44
115
|
- Ensure advisory lock CTE is MATERIALIZED on Postgres v12+ [\#179](https://github.com/bensheldon/good_job/pull/179) ([bensheldon](https://github.com/bensheldon))
|
45
116
|
- Ensure that deleted jobs are unlocked [\#178](https://github.com/bensheldon/good_job/pull/178) ([bensheldon](https://github.com/bensheldon))
|
117
|
+
- Fix job ordering for Rails 6.1 [\#174](https://github.com/bensheldon/good_job/pull/174) ([morgoth](https://github.com/morgoth))
|
46
118
|
|
47
119
|
**Closed issues:**
|
48
120
|
|
@@ -57,10 +129,6 @@
|
|
57
129
|
|
58
130
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.3...v1.3.4)
|
59
131
|
|
60
|
-
**Fixed bugs:**
|
61
|
-
|
62
|
-
- Fix job ordering for Rails 6.1 [\#174](https://github.com/bensheldon/good_job/pull/174) ([morgoth](https://github.com/morgoth))
|
63
|
-
|
64
132
|
## [v1.3.3](https://github.com/bensheldon/good_job/tree/v1.3.3) (2020-12-01)
|
65
133
|
|
66
134
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.2...v1.3.3)
|
@@ -101,6 +169,7 @@
|
|
101
169
|
- Fix Ruby 2.7 GH action by setting default bundler explicitly [\#166](https://github.com/bensheldon/good_job/pull/166) ([bensheldon](https://github.com/bensheldon))
|
102
170
|
- Cache ruby version explicitly in Github Action [\#165](https://github.com/bensheldon/good_job/pull/165) ([bensheldon](https://github.com/bensheldon))
|
103
171
|
- Update development dependencies, rubocop [\#164](https://github.com/bensheldon/good_job/pull/164) ([bensheldon](https://github.com/bensheldon))
|
172
|
+
- Fix intended constant hierarchy of GoodJob::Scheduler::ThreadPoolExecutor [\#158](https://github.com/bensheldon/good_job/pull/158) ([bensheldon](https://github.com/bensheldon))
|
104
173
|
- Add bin/test\_app executable for Rails debugging [\#157](https://github.com/bensheldon/good_job/pull/157) ([bensheldon](https://github.com/bensheldon))
|
105
174
|
- Extract Scheduler polling behavior to its own object [\#152](https://github.com/bensheldon/good_job/pull/152) ([bensheldon](https://github.com/bensheldon))
|
106
175
|
|
@@ -111,9 +180,6 @@
|
|
111
180
|
**Implemented enhancements:**
|
112
181
|
|
113
182
|
- Lengthen default poll interval from 1 to 5 seconds [\#156](https://github.com/bensheldon/good_job/pull/156) ([bensheldon](https://github.com/bensheldon))
|
114
|
-
|
115
|
-
**Merged pull requests:**
|
116
|
-
|
117
183
|
- Rename reperform\_jobs\_on\_standard\_error to retry\_on\_unhandled\_error [\#154](https://github.com/bensheldon/good_job/pull/154) ([morgoth](https://github.com/morgoth))
|
118
184
|
|
119
185
|
## [v1.2.6](https://github.com/bensheldon/good_job/tree/v1.2.6) (2020-09-29)
|
@@ -135,7 +201,6 @@
|
|
135
201
|
|
136
202
|
**Merged pull requests:**
|
137
203
|
|
138
|
-
- Fix intended constant hierarchy of GoodJob::Scheduler::ThreadPoolExecutor [\#158](https://github.com/bensheldon/good_job/pull/158) ([bensheldon](https://github.com/bensheldon))
|
139
204
|
- Add info how to setup basic auth for engine [\#153](https://github.com/bensheldon/good_job/pull/153) ([morgoth](https://github.com/morgoth))
|
140
205
|
- Add documentation for Dashboard Rails::Engine [\#149](https://github.com/bensheldon/good_job/pull/149) ([bensheldon](https://github.com/bensheldon))
|
141
206
|
- Style cleanup to Job error handling [\#147](https://github.com/bensheldon/good_job/pull/147) ([bensheldon](https://github.com/bensheldon))
|
@@ -224,11 +289,13 @@
|
|
224
289
|
**Implemented enhancements:**
|
225
290
|
|
226
291
|
- Run Github Action tests against Ruby 2.5, 2.6, 2.7 [\#100](https://github.com/bensheldon/good_job/issues/100)
|
292
|
+
- Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
|
227
293
|
|
228
294
|
**Fixed bugs:**
|
229
295
|
|
230
296
|
- Freezes puma on code change [\#95](https://github.com/bensheldon/good_job/issues/95)
|
231
297
|
- Ruby 2.7 keyword arguments warning [\#93](https://github.com/bensheldon/good_job/issues/93)
|
298
|
+
- Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
|
232
299
|
|
233
300
|
**Closed issues:**
|
234
301
|
|
@@ -238,10 +305,8 @@
|
|
238
305
|
|
239
306
|
- Use more ActiveRecord in Lockable and not connection.execute [\#102](https://github.com/bensheldon/good_job/pull/102) ([bensheldon](https://github.com/bensheldon))
|
240
307
|
- Run CI tests on Ruby 2.5, 2.6, and 2.7 [\#101](https://github.com/bensheldon/good_job/pull/101) ([arku](https://github.com/arku))
|
241
|
-
- Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
|
242
308
|
- Fix Ruby 2.7 keyword arguments warning [\#98](https://github.com/bensheldon/good_job/pull/98) ([arku](https://github.com/arku))
|
243
309
|
- Remove executor/reloader for less interlocking [\#97](https://github.com/bensheldon/good_job/pull/97) ([sj26](https://github.com/sj26))
|
244
|
-
- Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
|
245
310
|
- Add test for `rails g good\_job:install` [\#94](https://github.com/bensheldon/good_job/pull/94) ([arku](https://github.com/arku))
|
246
311
|
|
247
312
|
## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
|
data/README.md
CHANGED
@@ -35,8 +35,8 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
35
35
|
- [Command-line options](#command-line-options)
|
36
36
|
- [`good_job start`](#good_job-start)
|
37
37
|
- [`good_job cleanup_preserved_jobs`](#good_job-cleanup_preserved_jobs)
|
38
|
-
- [
|
39
|
-
- [Global options](#global-options)
|
38
|
+
- [Configuration options](#configuration-options)
|
39
|
+
- [Global options](#global-options)pter
|
40
40
|
- [Dashboard](#dashboard)
|
41
41
|
- [Go deeper](#go-deeper)
|
42
42
|
- [Exceptions, retries, and reliability](#exceptions-retries-and-reliability)
|
@@ -127,7 +127,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
127
127
|
## Compatibility
|
128
128
|
|
129
129
|
- **Ruby on Rails:** 5.2+
|
130
|
-
- **Ruby:** MRI 2.5+. JRuby 9.13+ (_JRuby's `activerecord-jdbcpostgresql-adapter` gem does not support Postgres LISTEN/NOTIFY)._
|
130
|
+
- **Ruby:** MRI 2.5+. JRuby 9.2.13+ (_JRuby's `activerecord-jdbcpostgresql-adapter` gem does not support Postgres LISTEN/NOTIFY)._
|
131
131
|
- **Postgres:** 9.6+
|
132
132
|
|
133
133
|
## Configuration
|
@@ -152,6 +152,9 @@ Options:
|
|
152
152
|
[--max-threads=COUNT] # Maximum number of threads to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)
|
153
153
|
[--queues=QUEUE_LIST] # Queues to work from. (env var: GOOD_JOB_QUEUES, default: *)
|
154
154
|
[--poll-interval=SECONDS] # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)
|
155
|
+
[--max-cache=COUNT] # Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)
|
156
|
+
[--daemonize] # Run as a background daemon (default: false)
|
157
|
+
[--pidfile=PIDFILE] # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)
|
155
158
|
|
156
159
|
Executes queued jobs.
|
157
160
|
|
@@ -159,6 +162,7 @@ All options can be configured with environment variables.
|
|
159
162
|
See option descriptions for the matching environment variable name.
|
160
163
|
|
161
164
|
== Configuring queues
|
165
|
+
|
162
166
|
Separate multiple queues with commas; exclude queues with a leading minus;
|
163
167
|
separate isolated execution pools with semicolons and threads with colons.
|
164
168
|
```
|
@@ -189,9 +193,31 @@ If you are preserving job records this way, use this command regularly
|
|
189
193
|
to delete old records and preserve space in your database.
|
190
194
|
```
|
191
195
|
|
192
|
-
###
|
196
|
+
### Configuration options
|
197
|
+
|
198
|
+
To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job`.
|
199
|
+
|
200
|
+
Additional configuration can be provided via `config.good_job.OPTION = ...` for example:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
# config/application.rb
|
204
|
+
|
205
|
+
config.active_job.queue_adapter = :good_job
|
206
|
+
|
207
|
+
# Configure options individually...
|
208
|
+
config.good_job.execution_mode = :async
|
209
|
+
config.good_job.max_threads = 5
|
210
|
+
config.good_job.poll_interval = 30 # seconds
|
193
211
|
|
194
|
-
|
212
|
+
# ...or all at once.
|
213
|
+
config.good_job = {
|
214
|
+
execution_mode: :async,
|
215
|
+
max_threads: 5,
|
216
|
+
poll_interval: 30,
|
217
|
+
}
|
218
|
+
```
|
219
|
+
|
220
|
+
Available configuration options are:
|
195
221
|
|
196
222
|
- `execution_mode` (symbol) specifies how and where jobs should be executed. You can also set this with the environment variable `GOOD_JOB_EXECUTION_MODE`. It can be any one of:
|
197
223
|
- `:inline` executes jobs immediately in whatever process queued them (usually the web server process). This should only be used in test and development environments.
|
@@ -200,18 +226,23 @@ To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job` o
|
|
200
226
|
- `max_threads` (integer) sets the maximum number of threads to use when `execution_mode` is set to `:async`. You can also set this with the environment variable `GOOD_JOB_MAX_THREADS`.
|
201
227
|
- `queues` (string) determines which queues to execute jobs from when `execution_mode` is set to `:async`. See the description of `good_job start` for more details on the format of this string. You can also set this with the environment variable `GOOD_JOB_QUEUES`.
|
202
228
|
- `poll_interval` (integer) sets the number of seconds between polls for jobs when `execution_mode` is set to `:async`. You can also set this with the environment variable `GOOD_JOB_POLL_INTERVAL`.
|
229
|
+
- `max_cache` (integer) sets the maximum number of scheduled jobs that will be stored in memory to reduce execution latency when also polling for scheduled jobs. Caching 10,000 scheduled jobs uses approximately 20MB of memory. You can also set this with the environment variable `GOOD_JOB_MAX_CACHE`.
|
203
230
|
|
204
|
-
|
231
|
+
By default, GoodJob configures the following execution modes per environment:
|
205
232
|
|
206
233
|
```ruby
|
234
|
+
|
207
235
|
# config/environments/development.rb
|
208
|
-
config.active_job.queue_adapter =
|
236
|
+
config.active_job.queue_adapter = :good_job
|
237
|
+
config.good_job.execution_mode = :inline
|
209
238
|
|
210
239
|
# config/environments/test.rb
|
211
|
-
config.active_job.queue_adapter =
|
240
|
+
config.active_job.queue_adapter = :good_job
|
241
|
+
config.good_job.execution_mode = :inline
|
212
242
|
|
213
243
|
# config/environments/production.rb
|
214
|
-
config.active_job.queue_adapter =
|
244
|
+
config.active_job.queue_adapter = :good_job
|
245
|
+
config.good_job.execution_mode = :external
|
215
246
|
```
|
216
247
|
|
217
248
|
### Global options
|
@@ -234,6 +265,8 @@ GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
|
|
234
265
|
|
235
266
|
### Dashboard
|
236
267
|
|
268
|
+

|
269
|
+
|
237
270
|
_🚧 GoodJob's dashboard is a work in progress. Please contribute ideas and code on [Github](https://github.com/bensheldon/good_job/issues)._
|
238
271
|
|
239
272
|
GoodJob includes a Dashboard as a mountable `Rails::Engine`.
|
@@ -440,14 +473,24 @@ pool: <%= [ENV.fetch("RAILS_MAX_THREADS", 5).to_i, ENV.fetch("GOOD_JOB_MAX_THREA
|
|
440
473
|
|
441
474
|
GoodJob can execute jobs "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:
|
442
475
|
|
443
|
-
-
|
476
|
+
- Via Rails configuration:
|
444
477
|
|
445
478
|
```ruby
|
446
479
|
# config/environments/production.rb
|
447
|
-
config.active_job.queue_adapter =
|
480
|
+
config.active_job.queue_adapter = :good_job
|
481
|
+
|
482
|
+
# To change the execution mode
|
483
|
+
config.good_job.execution_mode = :async
|
484
|
+
|
485
|
+
# Or with more configuration
|
486
|
+
config.good_job = {
|
487
|
+
execution_mode: :async,
|
488
|
+
max_threads: 4,
|
489
|
+
poll_interval: 30
|
490
|
+
}
|
448
491
|
```
|
449
492
|
|
450
|
-
- Or,
|
493
|
+
- Or, with environment variables:
|
451
494
|
|
452
495
|
```bash
|
453
496
|
$ GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server
|
data/exe/good_job
CHANGED
@@ -2,9 +2,9 @@ module ActiveJob # :nodoc:
|
|
2
2
|
module QueueAdapters # :nodoc:
|
3
3
|
# See {GoodJob::Adapter} for details.
|
4
4
|
class GoodJobAdapter < GoodJob::Adapter
|
5
|
-
def initialize(
|
6
|
-
configuration = GoodJob::Configuration.new(
|
7
|
-
super(execution_mode: configuration.rails_execution_mode
|
5
|
+
def initialize(**options)
|
6
|
+
configuration = GoodJob::Configuration.new(options, env: ENV)
|
7
|
+
super(**options.merge(execution_mode: configuration.rails_execution_mode))
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
data/lib/good_job.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
require "rails"
|
2
|
-
|
3
2
|
require "active_job"
|
4
3
|
require "active_job/queue_adapters"
|
5
4
|
|
6
5
|
require "zeitwerk"
|
7
|
-
|
8
|
-
loader
|
9
|
-
|
10
|
-
|
11
|
-
)
|
12
|
-
loader.
|
13
|
-
|
6
|
+
Zeitwerk::Loader.for_gem.tap do |loader|
|
7
|
+
loader.inflector.inflect({
|
8
|
+
"cli" => "CLI",
|
9
|
+
})
|
10
|
+
loader.ignore(File.join(File.dirname(__FILE__), "generators"))
|
11
|
+
loader.setup
|
12
|
+
end
|
14
13
|
|
15
14
|
require "good_job/railtie"
|
16
15
|
|
data/lib/good_job/adapter.rb
CHANGED
@@ -20,13 +20,22 @@ module GoodJob
|
|
20
20
|
# @param max_threads [nil, Integer] sets the number of threads per scheduler to use when +execution_mode+ is set to +:async+. The +queues+ parameter can specify a number of threads for each group of queues which will override this value. You can also set this with the environment variable +GOOD_JOB_MAX_THREADS+. Defaults to +5+.
|
21
21
|
# @param queues [nil, String] determines which queues to execute jobs from when +execution_mode+ is set to +:async+. See {file:README.md#optimize-queues-threads-and-processes} for more details on the format of this string. You can also set this with the environment variable +GOOD_JOB_QUEUES+. Defaults to +"*"+.
|
22
22
|
# @param poll_interval [nil, Integer] sets the number of seconds between polls for jobs when +execution_mode+ is set to +:async+. You can also set this with the environment variable +GOOD_JOB_POLL_INTERVAL+. Defaults to +1+.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil)
|
24
|
+
if caller[0..4].find { |c| c.include?("/config/application.rb") || c.include?("/config/environments/") }
|
25
|
+
ActiveSupport::Deprecation.warn(<<~DEPRECATION)
|
26
|
+
GoodJob no longer recommends creating a GoodJob::Adapter instance:
|
27
|
+
|
28
|
+
config.active_job.queue_adapter = GoodJob::Adapter.new...
|
29
|
+
|
30
|
+
Instead, configure GoodJob through configuration:
|
31
|
+
|
32
|
+
config.active_job.queue_adapter = :good_job
|
33
|
+
config.good_job.execution_mode = :#{execution_mode}
|
34
|
+
config.good_job.max_threads = #{max_threads}
|
35
|
+
config.good_job.poll_interval = #{poll_interval}
|
36
|
+
# etc...
|
37
|
+
|
38
|
+
DEPRECATION
|
30
39
|
end
|
31
40
|
|
32
41
|
configuration = GoodJob::Configuration.new(
|
@@ -41,10 +50,10 @@ module GoodJob
|
|
41
50
|
@execution_mode = configuration.execution_mode
|
42
51
|
raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
|
43
52
|
|
44
|
-
if
|
45
|
-
@notifier =
|
53
|
+
if execute_async? # rubocop:disable Style/GuardClause
|
54
|
+
@notifier = GoodJob::Notifier.new
|
46
55
|
@poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
47
|
-
@scheduler =
|
56
|
+
@scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: Rails.application.initialized?)
|
48
57
|
@notifier.recipients << [@scheduler, :create_thread]
|
49
58
|
@poller.recipients << [@scheduler, :create_thread]
|
50
59
|
end
|
@@ -76,10 +85,13 @@ module GoodJob
|
|
76
85
|
ensure
|
77
86
|
good_job.advisory_unlock
|
78
87
|
end
|
79
|
-
|
88
|
+
else
|
89
|
+
job_state = { queue_name: good_job.queue_name }
|
90
|
+
job_state[:scheduled_at] = good_job.scheduled_at if good_job.scheduled_at
|
80
91
|
|
81
|
-
|
82
|
-
|
92
|
+
executed_locally = execute_async? && @scheduler.create_thread(job_state)
|
93
|
+
Notifier.notify(job_state) unless executed_locally
|
94
|
+
end
|
83
95
|
|
84
96
|
good_job
|
85
97
|
end
|
@@ -108,11 +120,5 @@ module GoodJob
|
|
108
120
|
def execute_inline?
|
109
121
|
@execution_mode == :inline
|
110
122
|
end
|
111
|
-
|
112
|
-
# (deprecated) Whether in +:inline+ execution mode.
|
113
|
-
def inline?
|
114
|
-
ActiveSupport::Deprecation.warn('GoodJob::Adapter::inline? is deprecated; use GoodJob::Adapter::execute_inline? instead')
|
115
|
-
execute_inline?
|
116
|
-
end
|
117
123
|
end
|
118
124
|
end
|
data/lib/good_job/cli.rb
CHANGED
@@ -15,6 +15,11 @@ module GoodJob
|
|
15
15
|
# Requiring this loads the application's configuration and classes.
|
16
16
|
RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
|
17
17
|
|
18
|
+
# @!visibility private
|
19
|
+
def self.exit_on_failure?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
18
23
|
# @!macro thor.desc
|
19
24
|
# @!method $1
|
20
25
|
# @return [void]
|
@@ -27,7 +32,8 @@ module GoodJob
|
|
27
32
|
See option descriptions for the matching environment variable name.
|
28
33
|
|
29
34
|
== Configuring queues
|
30
|
-
|
35
|
+
|
36
|
+
Separate multiple queues with commas; exclude queues with a leading minus;
|
31
37
|
separate isolated execution pools with semicolons and threads with colons.
|
32
38
|
|
33
39
|
DESCRIPTION
|
@@ -43,10 +49,22 @@ module GoodJob
|
|
43
49
|
type: :numeric,
|
44
50
|
banner: 'SECONDS',
|
45
51
|
desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
|
52
|
+
method_option :max_cache,
|
53
|
+
type: :numeric,
|
54
|
+
banner: 'COUNT',
|
55
|
+
desc: "Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)"
|
56
|
+
method_option :daemonize,
|
57
|
+
type: :boolean,
|
58
|
+
desc: "Run as a background daemon (default: false)"
|
59
|
+
method_option :pidfile,
|
60
|
+
type: :string,
|
61
|
+
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
|
46
62
|
def start
|
47
63
|
set_up_application!
|
48
64
|
configuration = GoodJob::Configuration.new(options)
|
49
65
|
|
66
|
+
Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?
|
67
|
+
|
50
68
|
notifier = GoodJob::Notifier.new
|
51
69
|
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
52
70
|
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
@@ -8,20 +8,21 @@ module GoodJob
|
|
8
8
|
# Default number of threads to use per {Scheduler}
|
9
9
|
DEFAULT_MAX_THREADS = 5
|
10
10
|
# Default number of seconds between polls for jobs
|
11
|
-
DEFAULT_POLL_INTERVAL =
|
11
|
+
DEFAULT_POLL_INTERVAL = 10
|
12
|
+
# Default number of threads to use per {Scheduler}
|
13
|
+
DEFAULT_MAX_CACHE = 10000
|
12
14
|
# Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
|
13
15
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
|
14
16
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
attr_reader :options, :env
|
17
|
+
# The options that were explicitly set when initializing +Configuration+.
|
18
|
+
# @return [Hash]
|
19
|
+
attr_reader :options
|
20
|
+
|
21
|
+
# The environment from which to read GoodJob's environment variables. By
|
22
|
+
# default, this is the current process's environment, but it can be set
|
23
|
+
# to something else in {#initialize}.
|
24
|
+
# @return [Hash]
|
25
|
+
attr_reader :env
|
25
26
|
|
26
27
|
# @param options [Hash] Any explicitly specified configuration options to
|
27
28
|
# use. Keys are symbols that match the various methods on this class.
|
@@ -43,8 +44,12 @@ module GoodJob
|
|
43
44
|
# Value to use if none was specified in the configuration.
|
44
45
|
# @return [Symbol]
|
45
46
|
def execution_mode(default: :external)
|
46
|
-
if
|
47
|
+
if defined?(GOOD_JOB_WITHIN_CLI) && GOOD_JOB_WITHIN_CLI
|
48
|
+
:external
|
49
|
+
elsif options[:execution_mode]
|
47
50
|
options[:execution_mode]
|
51
|
+
elsif rails_config[:execution_mode]
|
52
|
+
rails_config[:execution_mode]
|
48
53
|
elsif env['GOOD_JOB_EXECUTION_MODE'].present?
|
49
54
|
env['GOOD_JOB_EXECUTION_MODE'].to_sym
|
50
55
|
else
|
@@ -72,6 +77,7 @@ module GoodJob
|
|
72
77
|
def max_threads
|
73
78
|
(
|
74
79
|
options[:max_threads] ||
|
80
|
+
rails_config[:max_threads] ||
|
75
81
|
env['GOOD_JOB_MAX_THREADS'] ||
|
76
82
|
env['RAILS_MAX_THREADS'] ||
|
77
83
|
DEFAULT_MAX_THREADS
|
@@ -85,6 +91,7 @@ module GoodJob
|
|
85
91
|
# @return [String]
|
86
92
|
def queue_string
|
87
93
|
options[:queues] ||
|
94
|
+
rails_config[:queues] ||
|
88
95
|
env['GOOD_JOB_QUEUES'] ||
|
89
96
|
'*'
|
90
97
|
end
|
@@ -96,17 +103,55 @@ module GoodJob
|
|
96
103
|
def poll_interval
|
97
104
|
(
|
98
105
|
options[:poll_interval] ||
|
106
|
+
rails_config[:poll_interval] ||
|
99
107
|
env['GOOD_JOB_POLL_INTERVAL'] ||
|
100
108
|
DEFAULT_POLL_INTERVAL
|
101
109
|
).to_i
|
102
110
|
end
|
103
111
|
|
112
|
+
# The maximum number of future-scheduled jobs to store in memory.
|
113
|
+
# Storing future-scheduled jobs in memory reduces execution latency
|
114
|
+
# at the cost of increased memory usage. 10,000 stored jobs = ~20MB.
|
115
|
+
# @return [Integer]
|
116
|
+
def max_cache
|
117
|
+
(
|
118
|
+
options[:max_cache] ||
|
119
|
+
rails_config[:max_cache] ||
|
120
|
+
env['GOOD_JOB_MAX_CACHE'] ||
|
121
|
+
DEFAULT_MAX_CACHE
|
122
|
+
).to_i
|
123
|
+
end
|
124
|
+
|
125
|
+
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
126
|
+
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
127
|
+
# @return [Integer]
|
104
128
|
def cleanup_preserved_jobs_before_seconds_ago
|
105
129
|
(
|
106
130
|
options[:before_seconds_ago] ||
|
131
|
+
rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
|
107
132
|
env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
|
108
133
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
|
109
134
|
).to_i
|
110
135
|
end
|
136
|
+
|
137
|
+
# Tests whether to daemonize the process.
|
138
|
+
# @return [Boolean]
|
139
|
+
def daemonize?
|
140
|
+
options[:daemonize] || false
|
141
|
+
end
|
142
|
+
|
143
|
+
# Path of the pidfile to create when running as a daemon.
|
144
|
+
# @return [Pathname,String]
|
145
|
+
def pidfile
|
146
|
+
options[:pidfile] ||
|
147
|
+
env['GOOD_JOB_PIDFILE'] ||
|
148
|
+
Rails.application.root.join('tmp', 'pids', 'good_job.pid')
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def rails_config
|
154
|
+
Rails.application.config.good_job
|
155
|
+
end
|
111
156
|
end
|
112
157
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module GoodJob
|
2
|
+
#
|
3
|
+
# Manages daemonization of the current process.
|
4
|
+
#
|
5
|
+
class Daemon
|
6
|
+
# The path of the generated pidfile.
|
7
|
+
# @return [Pathname,String]
|
8
|
+
attr_reader :pidfile
|
9
|
+
|
10
|
+
# @param pidfile [Pathname,String] Pidfile path
|
11
|
+
def initialize(pidfile:)
|
12
|
+
@pidfile = pidfile
|
13
|
+
end
|
14
|
+
|
15
|
+
# Daemonizes the current process and writes out a pidfile.
|
16
|
+
def daemonize
|
17
|
+
check_pid
|
18
|
+
Process.daemon
|
19
|
+
write_pid
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def write_pid
|
25
|
+
File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
|
26
|
+
at_exit { File.delete(pidfile) if File.exist?(pidfile) }
|
27
|
+
rescue Errno::EEXIST
|
28
|
+
check_pid
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_pid
|
33
|
+
File.delete(pidfile) if File.exist?(pidfile)
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_pid
|
37
|
+
case pid_status(pidfile)
|
38
|
+
when :running, :not_owned
|
39
|
+
abort "A server is already running. Check #{pidfile}"
|
40
|
+
when :dead
|
41
|
+
File.delete(pidfile)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def pid_status(pidfile)
|
46
|
+
return :exited unless File.exist?(pidfile)
|
47
|
+
|
48
|
+
pid = ::File.read(pidfile).to_i
|
49
|
+
return :dead if pid.zero?
|
50
|
+
|
51
|
+
Process.kill(0, pid) # check process status
|
52
|
+
:running
|
53
|
+
rescue Errno::ESRCH
|
54
|
+
:dead
|
55
|
+
rescue Errno::EPERM
|
56
|
+
:not_owned
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/good_job/job.rb
CHANGED
@@ -72,6 +72,12 @@ module GoodJob
|
|
72
72
|
# @return [ActiveRecord::Relation]
|
73
73
|
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
74
74
|
|
75
|
+
# Order jobs by scheduled (unscheduled or soonest first).
|
76
|
+
# @!method schedule_ordered
|
77
|
+
# @!scope class
|
78
|
+
# @return [ActiveRecord::Relation]
|
79
|
+
scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
|
80
|
+
|
75
81
|
# Get Jobs were completed before the given timestamp. If no timestamp is
|
76
82
|
# provided, get all jobs that have been completed. By default, GoodJob
|
77
83
|
# deletes jobs after they are completed and this will find no jobs.
|
@@ -147,6 +153,23 @@ module GoodJob
|
|
147
153
|
[good_job, result, error] if good_job
|
148
154
|
end
|
149
155
|
|
156
|
+
# Fetches the scheduled execution time of the next eligible Job(s).
|
157
|
+
# @return [Array<(DateTime)>]
|
158
|
+
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
159
|
+
query = advisory_unlocked.unfinished.schedule_ordered
|
160
|
+
|
161
|
+
after ||= Time.current
|
162
|
+
after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
|
163
|
+
after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
164
|
+
|
165
|
+
if now_limit&.positive?
|
166
|
+
now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
|
167
|
+
now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
168
|
+
end
|
169
|
+
|
170
|
+
Array(now_at) + after_at
|
171
|
+
end
|
172
|
+
|
150
173
|
# Places an ActiveJob job on a queue by creating a new {Job} record.
|
151
174
|
# @param active_job [ActiveJob::Base]
|
152
175
|
# The job to enqueue.
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'concurrent/delay'
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
#
|
5
|
+
# JobPerformer queries the database for jobs and performs them on behalf of a
|
6
|
+
# {Scheduler}. It mainly functions as glue between a {Scheduler} and the jobs
|
7
|
+
# it should be executing.
|
8
|
+
#
|
9
|
+
# The JobPerformer must be safe to execute across multiple threads.
|
10
|
+
#
|
11
|
+
class JobPerformer
|
12
|
+
# @param queue_string [String] Queues to execute jobs from
|
13
|
+
def initialize(queue_string)
|
14
|
+
@queue_string = queue_string
|
15
|
+
|
16
|
+
@job_query = Concurrent::Delay.new { GoodJob::Job.queue_string(queue_string) }
|
17
|
+
@parsed_queues = Concurrent::Delay.new { GoodJob::Job.queue_parser(queue_string) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# A meaningful name to identify the performer in logs and for debugging.
|
21
|
+
# @return [String] The queues from which Jobs are worked
|
22
|
+
def name
|
23
|
+
@queue_string
|
24
|
+
end
|
25
|
+
|
26
|
+
# Perform the next eligible job
|
27
|
+
# @return [nil, Object] Returns job result or +nil+ if no job was found
|
28
|
+
def next
|
29
|
+
job_query.perform_with_advisory_lock
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tests whether this performer should be used in GoodJob's current state.
|
33
|
+
#
|
34
|
+
# For example, state will be a LISTEN/NOTIFY message that is passed down
|
35
|
+
# from the Notifier to the Scheduler. The Scheduler is able to ask
|
36
|
+
# its performer "does this message relate to you?", and if not, ignore it
|
37
|
+
# to minimize thread wake-ups, database queries, and thundering herds.
|
38
|
+
#
|
39
|
+
# @return [Boolean] whether the performer's {#next} method should be
|
40
|
+
# called in the current state.
|
41
|
+
def next?(state = {})
|
42
|
+
return true unless state[:queue_name]
|
43
|
+
|
44
|
+
if parsed_queues[:exclude]
|
45
|
+
parsed_queues[:exclude].exclude?(state[:queue_name])
|
46
|
+
elsif parsed_queues[:include]
|
47
|
+
parsed_queues[:include].include?(state[:queue_name])
|
48
|
+
else
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# The Returns timestamps of when next tasks may be available.
|
54
|
+
# @param count [Integer] number of timestamps to return
|
55
|
+
# @param count [DateTime, Time, nil] jobs scheduled after this time
|
56
|
+
# @return [Array<(Time, Timestamp)>, nil]
|
57
|
+
def next_at(after: nil, limit: nil, now_limit: nil)
|
58
|
+
job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
attr_reader :queue_string
|
64
|
+
|
65
|
+
def job_query
|
66
|
+
@job_query.value
|
67
|
+
end
|
68
|
+
|
69
|
+
def parsed_queues
|
70
|
+
@parsed_queues.value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/good_job/lockable.rb
CHANGED
@@ -92,8 +92,6 @@ module GoodJob
|
|
92
92
|
# @return [ActiveRecord::Relation]
|
93
93
|
scope :owns_advisory_locked, -> { joins_advisory_locks.where('"pg_locks"."pid" = pg_backend_pid()') }
|
94
94
|
|
95
|
-
# @!attribute [r] create_with_advisory_lock
|
96
|
-
# @return [Boolean]
|
97
95
|
# Whether an advisory lock should be acquired in the same transaction
|
98
96
|
# that created the record.
|
99
97
|
#
|
@@ -107,6 +105,8 @@ module GoodJob
|
|
107
105
|
# record = MyLockableRecord.create(create_with_advisory_lock: true)
|
108
106
|
# record.advisory_locked?
|
109
107
|
# => true
|
108
|
+
#
|
109
|
+
# @return [Boolean]
|
110
110
|
attr_accessor :create_with_advisory_lock
|
111
111
|
|
112
112
|
after_create -> { advisory_lock }, if: :create_with_advisory_lock
|
data/lib/good_job/notifier.rb
CHANGED
@@ -9,6 +9,7 @@ module GoodJob # :nodoc:
|
|
9
9
|
# When a message is received, the notifier passes the message to each of its recipients.
|
10
10
|
#
|
11
11
|
class Notifier
|
12
|
+
# Raised if the Database adapter does not implement LISTEN.
|
12
13
|
AdapterCannotListenError = Class.new(StandardError)
|
13
14
|
|
14
15
|
# Default Postgres channel for LISTEN/NOTIFY
|
data/lib/good_job/poller.rb
CHANGED
@@ -19,6 +19,9 @@ module GoodJob # :nodoc:
|
|
19
19
|
# @return [array<GoodJob:Poller>]
|
20
20
|
cattr_reader :instances, default: [], instance_reader: false
|
21
21
|
|
22
|
+
# Creates GoodJob::Poller from a GoodJob::Configuration instance.
|
23
|
+
# @param configuration [GoodJob::Configuration]
|
24
|
+
# @return [GoodJob::Poller]
|
22
25
|
def self.from_configuration(configuration)
|
23
26
|
GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
24
27
|
end
|
data/lib/good_job/railtie.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
module GoodJob
|
2
2
|
# Ruby on Rails integration.
|
3
3
|
class Railtie < ::Rails::Railtie
|
4
|
-
|
5
|
-
|
4
|
+
config.good_job = ActiveSupport::OrderedOptions.new
|
5
|
+
|
6
|
+
initializer "good_job.logger" do |_app|
|
7
|
+
ActiveSupport.on_load(:good_job) do
|
8
|
+
self.logger = ::Rails.logger
|
9
|
+
end
|
6
10
|
GoodJob::LogSubscriber.attach_to :good_job
|
7
11
|
end
|
8
12
|
|
@@ -15,5 +19,9 @@ module GoodJob
|
|
15
19
|
GoodJob::CurrentExecution.error_on_discard = event.payload[:error]
|
16
20
|
end
|
17
21
|
end
|
22
|
+
|
23
|
+
config.after_initialize do
|
24
|
+
GoodJob::Scheduler.instances.each(&:warm_cache)
|
25
|
+
end
|
18
26
|
end
|
19
27
|
end
|
data/lib/good_job/scheduler.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "concurrent/executor/thread_pool_executor"
|
2
|
-
require "concurrent/
|
2
|
+
require "concurrent/executor/timer_set"
|
3
|
+
require "concurrent/scheduled_task"
|
3
4
|
require "concurrent/utility/processor_counter"
|
4
5
|
|
5
6
|
module GoodJob # :nodoc:
|
@@ -8,7 +9,7 @@ module GoodJob # :nodoc:
|
|
8
9
|
# periodically checking for available tasks, executing tasks within a thread,
|
9
10
|
# and efficiently scaling active threads.
|
10
11
|
#
|
11
|
-
# Every scheduler has a single {
|
12
|
+
# Every scheduler has a single {JobPerformer} that will execute tasks.
|
12
13
|
# The scheduler is responsible for calling its performer efficiently across threads managed by an instance of +Concurrent::ThreadPoolExecutor+.
|
13
14
|
# If a performer does not have work, the thread will go to sleep.
|
14
15
|
# The scheduler maintains an instance of +Concurrent::TimerTask+, which wakes sleeping threads and causes them to check whether the performer has new work.
|
@@ -22,7 +23,7 @@ module GoodJob # :nodoc:
|
|
22
23
|
max_threads: Configuration::DEFAULT_MAX_THREADS,
|
23
24
|
auto_terminate: true,
|
24
25
|
idletime: 60,
|
25
|
-
max_queue:
|
26
|
+
max_queue: Configuration::DEFAULT_MAX_THREADS,
|
26
27
|
fallback_policy: :discard,
|
27
28
|
}.freeze
|
28
29
|
|
@@ -34,26 +35,20 @@ module GoodJob # :nodoc:
|
|
34
35
|
|
35
36
|
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
36
37
|
# @param configuration [GoodJob::Configuration]
|
38
|
+
# @param warm_cache_on_initialize [Boolean]
|
37
39
|
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
38
|
-
def self.from_configuration(configuration)
|
40
|
+
def self.from_configuration(configuration, warm_cache_on_initialize: true)
|
39
41
|
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
40
42
|
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
41
43
|
max_threads = (max_threads || configuration.max_threads).to_i
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
else
|
51
|
-
true
|
52
|
-
end
|
53
|
-
end
|
54
|
-
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
|
55
|
-
|
56
|
-
GoodJob::Scheduler.new(job_performer, max_threads: max_threads)
|
45
|
+
job_performer = GoodJob::JobPerformer.new(queue_string)
|
46
|
+
GoodJob::Scheduler.new(
|
47
|
+
job_performer,
|
48
|
+
max_threads: max_threads,
|
49
|
+
max_cache: configuration.max_cache,
|
50
|
+
warm_cache_on_initialize: warm_cache_on_initialize
|
51
|
+
)
|
57
52
|
end
|
58
53
|
|
59
54
|
if schedulers.size > 1
|
@@ -63,20 +58,27 @@ module GoodJob # :nodoc:
|
|
63
58
|
end
|
64
59
|
end
|
65
60
|
|
66
|
-
# @param performer [GoodJob::
|
61
|
+
# @param performer [GoodJob::JobPerformer]
|
67
62
|
# @param max_threads [Numeric, nil] number of seconds between polls for jobs
|
68
|
-
|
63
|
+
# @param max_cache [Numeric, nil] maximum number of scheduled jobs to cache in memory
|
64
|
+
# @param warm_cache_on_initialize [Boolean] whether to warm the cache immediately
|
65
|
+
def initialize(performer, max_threads: nil, max_cache: nil, warm_cache_on_initialize: true)
|
69
66
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
70
67
|
|
71
68
|
self.class.instances << self
|
72
69
|
|
73
70
|
@performer = performer
|
74
71
|
|
72
|
+
@max_cache = max_cache || 0
|
75
73
|
@pool_options = DEFAULT_POOL_OPTIONS.dup
|
76
|
-
|
74
|
+
if max_threads.present?
|
75
|
+
@pool_options[:max_threads] = max_threads
|
76
|
+
@pool_options[:max_queue] = max_threads
|
77
|
+
end
|
77
78
|
@pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]})"
|
78
79
|
|
79
80
|
create_pool
|
81
|
+
warm_cache if warm_cache_on_initialize
|
80
82
|
end
|
81
83
|
|
82
84
|
# Shut down the scheduler.
|
@@ -91,6 +93,8 @@ module GoodJob # :nodoc:
|
|
91
93
|
|
92
94
|
instrument("scheduler_shutdown_start", { wait: wait })
|
93
95
|
instrument("scheduler_shutdown", { wait: wait }) do
|
96
|
+
@timer_set.shutdown
|
97
|
+
|
94
98
|
@pool.shutdown
|
95
99
|
@pool.wait_for_termination if wait
|
96
100
|
# TODO: Should be killed if wait is not true
|
@@ -111,6 +115,7 @@ module GoodJob # :nodoc:
|
|
111
115
|
instrument("scheduler_restart_pools") do
|
112
116
|
shutdown(wait: wait) unless shutdown?
|
113
117
|
create_pool
|
118
|
+
warm_cache
|
114
119
|
end
|
115
120
|
end
|
116
121
|
|
@@ -121,10 +126,30 @@ module GoodJob # :nodoc:
|
|
121
126
|
# Returns +true+ if the performer started executing work.
|
122
127
|
# Returns +false+ if the performer decides not to attempt to execute a task based on the +state+ that is passed to it.
|
123
128
|
def create_thread(state = nil)
|
124
|
-
return nil unless @pool.running?
|
125
|
-
|
129
|
+
return nil unless @pool.running?
|
130
|
+
|
131
|
+
if state
|
132
|
+
return false unless @performer.next?(state)
|
133
|
+
|
134
|
+
if state[:scheduled_at]
|
135
|
+
scheduled_at = if state[:scheduled_at].is_a? String
|
136
|
+
Time.zone.parse state[:scheduled_at]
|
137
|
+
else
|
138
|
+
state[:scheduled_at]
|
139
|
+
end
|
140
|
+
delay = [(scheduled_at - Time.current).to_f, 0].max
|
141
|
+
end
|
142
|
+
end
|
126
143
|
|
127
|
-
|
144
|
+
delay ||= 0
|
145
|
+
run_now = delay <= 0.01
|
146
|
+
if run_now
|
147
|
+
return nil unless @pool.ready_worker_count.positive?
|
148
|
+
elsif @max_cache.positive?
|
149
|
+
return nil unless remaining_cache_count.positive?
|
150
|
+
end
|
151
|
+
|
152
|
+
future = Concurrent::ScheduledTask.new(delay, args: [@performer], executor: @pool, timer_set: timer_set) do |performer|
|
128
153
|
output = nil
|
129
154
|
Rails.application.executor.wrap { output = performer.next }
|
130
155
|
output
|
@@ -132,7 +157,7 @@ module GoodJob # :nodoc:
|
|
132
157
|
future.add_observer(self, :task_observer)
|
133
158
|
future.execute
|
134
159
|
|
135
|
-
true
|
160
|
+
run_now ? true : nil
|
136
161
|
end
|
137
162
|
|
138
163
|
# Invoked on completion of ThreadPoolExecutor task
|
@@ -141,17 +166,53 @@ module GoodJob # :nodoc:
|
|
141
166
|
def task_observer(time, output, thread_error)
|
142
167
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
143
168
|
instrument("finished_job_task", { result: output, error: thread_error, time: time })
|
144
|
-
|
169
|
+
create_task if output
|
170
|
+
end
|
171
|
+
|
172
|
+
def warm_cache
|
173
|
+
return if @max_cache.zero?
|
174
|
+
|
175
|
+
@performer.next_at(
|
176
|
+
limit: @max_cache,
|
177
|
+
now_limit: @pool_options[:max_threads]
|
178
|
+
).each do |scheduled_at|
|
179
|
+
create_thread({ scheduled_at: scheduled_at })
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def stats
|
184
|
+
{
|
185
|
+
name: @performer.name,
|
186
|
+
max_threads: @pool_options[:max_threads],
|
187
|
+
active_threads: @pool_options[:max_threads] - @pool.ready_worker_count,
|
188
|
+
available_threads: @pool.ready_worker_count,
|
189
|
+
max_cache: @max_cache,
|
190
|
+
active_cache: cache_count,
|
191
|
+
available_cache: remaining_cache_count,
|
192
|
+
}
|
145
193
|
end
|
146
194
|
|
147
195
|
private
|
148
196
|
|
197
|
+
attr_reader :timer_set
|
198
|
+
|
149
199
|
def create_pool
|
150
200
|
instrument("scheduler_create_pool", { performer_name: @performer.name, max_threads: @pool_options[:max_threads] }) do
|
201
|
+
@timer_set = Concurrent::TimerSet.new
|
151
202
|
@pool = ThreadPoolExecutor.new(@pool_options)
|
152
203
|
end
|
153
204
|
end
|
154
205
|
|
206
|
+
def create_task(delay = 0)
|
207
|
+
future = Concurrent::ScheduledTask.new(delay, args: [@performer], executor: @pool, timer_set: timer_set) do |performer|
|
208
|
+
output = nil
|
209
|
+
Rails.application.executor.wrap { output = performer.next }
|
210
|
+
output
|
211
|
+
end
|
212
|
+
future.add_observer(self, :task_observer)
|
213
|
+
future.execute
|
214
|
+
end
|
215
|
+
|
155
216
|
def instrument(name, payload = {}, &block)
|
156
217
|
payload = payload.reverse_merge({
|
157
218
|
scheduler: self,
|
@@ -162,6 +223,14 @@ module GoodJob # :nodoc:
|
|
162
223
|
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
163
224
|
end
|
164
225
|
|
226
|
+
def cache_count
|
227
|
+
timer_set.instance_variable_get(:@queue).length
|
228
|
+
end
|
229
|
+
|
230
|
+
def remaining_cache_count
|
231
|
+
@max_cache - cache_count
|
232
|
+
end
|
233
|
+
|
165
234
|
# Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
|
166
235
|
# @private
|
167
236
|
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
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.
|
4
|
+
version: 1.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -357,12 +357,13 @@ files:
|
|
357
357
|
- lib/good_job/cli.rb
|
358
358
|
- lib/good_job/configuration.rb
|
359
359
|
- lib/good_job/current_execution.rb
|
360
|
+
- lib/good_job/daemon.rb
|
360
361
|
- lib/good_job/job.rb
|
362
|
+
- lib/good_job/job_performer.rb
|
361
363
|
- lib/good_job/lockable.rb
|
362
364
|
- lib/good_job/log_subscriber.rb
|
363
365
|
- lib/good_job/multi_scheduler.rb
|
364
366
|
- lib/good_job/notifier.rb
|
365
|
-
- lib/good_job/performer.rb
|
366
367
|
- lib/good_job/poller.rb
|
367
368
|
- lib/good_job/railtie.rb
|
368
369
|
- lib/good_job/scheduler.rb
|
@@ -399,7 +400,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
399
400
|
- !ruby/object:Gem::Version
|
400
401
|
version: '0'
|
401
402
|
requirements: []
|
402
|
-
rubygems_version: 3.2.
|
403
|
+
rubygems_version: 3.2.4
|
403
404
|
signing_key:
|
404
405
|
specification_version: 4
|
405
406
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
data/lib/good_job/performer.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
module GoodJob
|
2
|
-
#
|
3
|
-
# Performer queries the database for jobs and performs them on behalf of a
|
4
|
-
# {Scheduler}. It mainly functions as glue between a {Scheduler} and the jobs
|
5
|
-
# it should be executing.
|
6
|
-
#
|
7
|
-
# The Performer enforces a callable that does not rely on scoped/closure
|
8
|
-
# variables because they might not be available when executed in a different
|
9
|
-
# thread.
|
10
|
-
#
|
11
|
-
class Performer
|
12
|
-
# @!attribute [r] name
|
13
|
-
# @return [String]
|
14
|
-
# a meaningful name to identify the performer in logs and for debugging.
|
15
|
-
# This is usually set to the list of queues the performer will query,
|
16
|
-
# e.g. +"-transactional_messages,batch_processing"+.
|
17
|
-
attr_reader :name
|
18
|
-
|
19
|
-
# @param target [Object]
|
20
|
-
# An object that can perform jobs. It must respond to +method_name+ by
|
21
|
-
# finding and performing jobs and is usually a {Job} query,
|
22
|
-
# e.g. +GoodJob::Job.where(queue_name: ['queue1', 'queue2'])+.
|
23
|
-
# @param method_name [Symbol]
|
24
|
-
# The name of a method on +target+ that finds and performs jobs.
|
25
|
-
# @param name [String]
|
26
|
-
# A name for the performer to be used in logs and for debugging.
|
27
|
-
# @param filter [#call]
|
28
|
-
# Used to determine whether the performer should be used in GoodJob's
|
29
|
-
# current state. GoodJob state is a +Hash+ that will be passed as the
|
30
|
-
# first argument to +filter+ and includes info like the current queue.
|
31
|
-
def initialize(target, method_name, name: nil, filter: nil)
|
32
|
-
@target = target
|
33
|
-
@method_name = method_name
|
34
|
-
@name = name
|
35
|
-
@filter = filter
|
36
|
-
end
|
37
|
-
|
38
|
-
# Find and perform any eligible jobs.
|
39
|
-
def next
|
40
|
-
@target.public_send(@method_name)
|
41
|
-
end
|
42
|
-
|
43
|
-
# Tests whether this performer should be used in GoodJob's current state by
|
44
|
-
# calling the +filter+ callable set in {#initialize}. Always returns +true+
|
45
|
-
# if there is no filter.
|
46
|
-
#
|
47
|
-
# For example, state will be a LISTEN/NOTIFY message that is passed down
|
48
|
-
# from the Notifier to the Scheduler. The Scheduler is able to ask
|
49
|
-
# its performer "does this message relate to you?", and if not, ignore it
|
50
|
-
# to minimize thread wake-ups, database queries, and thundering herds.
|
51
|
-
#
|
52
|
-
# @return [Boolean] whether the performer's {#next} method should be
|
53
|
-
# called in the current state.
|
54
|
-
def next?(state = {})
|
55
|
-
return true unless @filter.respond_to?(:call)
|
56
|
-
|
57
|
-
@filter.call(state)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|