good_job 1.3.6 → 1.7.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 +72 -5
- data/README.md +61 -11
- 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 +21 -8
- data/lib/good_job/notifier.rb +7 -0
- data/lib/good_job/poller.rb +3 -0
- data/lib/good_job/railtie.rb +10 -2
- data/lib/good_job/scheduler.rb +87 -28
- data/lib/good_job/version.rb +1 -1
- metadata +5 -46
- 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: ba54d04d3afa9fea7af913fb6e04f3bdbc104f47a57b629001071da7fcd4ed55
|
4
|
+
data.tar.gz: 2bd4dd5be31a15c43d58f0ab7cd33830834e1e2bcd0506445258aa75d4cc98e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ade9720ef3918e10d129e886411b819e8fd96614a299552d68287ed43fcab2997057b8c63ee26b35ac321cf7d58a055c7cf5b14f74f3c887b14f59934537be8
|
7
|
+
data.tar.gz: 9f81f5e7faacbe1b6a4999fa82afa6eb03675472c0237616f6f570c235f72c8c925fbea0ff526726bb6542c795ef47feaa61b5e9dd00520d6a38fbfa4b7463a5
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,75 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.7.0](https://github.com/bensheldon/good_job/tree/v1.7.0) (2021-01-25)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.6.0...v1.7.0)
|
6
|
+
|
7
|
+
**Merged pull requests:**
|
8
|
+
|
9
|
+
- 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))
|
10
|
+
|
11
|
+
## [v1.6.0](https://github.com/bensheldon/good_job/tree/v1.6.0) (2021-01-22)
|
12
|
+
|
13
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.5.0...v1.6.0)
|
14
|
+
|
15
|
+
**Implemented enhancements:**
|
16
|
+
|
17
|
+
- Running as a daemon [\#88](https://github.com/bensheldon/good_job/issues/88)
|
18
|
+
- Add daemonize option to CLI [\#202](https://github.com/bensheldon/good_job/pull/202) ([bensheldon](https://github.com/bensheldon))
|
19
|
+
|
20
|
+
**Closed issues:**
|
21
|
+
|
22
|
+
- Rails 6.1 & async - `queue\_parser': undefined method `first' for "\*":String \(NoMethodError\) [\#195](https://github.com/bensheldon/good_job/issues/195)
|
23
|
+
|
24
|
+
**Merged pull requests:**
|
25
|
+
|
26
|
+
- Add scripts directory for benchmarking and dev tasks [\#204](https://github.com/bensheldon/good_job/pull/204) ([bensheldon](https://github.com/bensheldon))
|
27
|
+
- Fix YARD attr\_ declarations for documentation [\#203](https://github.com/bensheldon/good_job/pull/203) ([bensheldon](https://github.com/bensheldon))
|
28
|
+
- Remove Appraisal gemfile locks [\#201](https://github.com/bensheldon/good_job/pull/201) ([bensheldon](https://github.com/bensheldon))
|
29
|
+
|
30
|
+
## [v1.5.0](https://github.com/bensheldon/good_job/tree/v1.5.0) (2021-01-18)
|
31
|
+
|
32
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.4.1...v1.5.0)
|
33
|
+
|
34
|
+
**Implemented enhancements:**
|
35
|
+
|
36
|
+
- Create Web UI Dashboard [\#50](https://github.com/bensheldon/good_job/issues/50)
|
37
|
+
|
38
|
+
**Closed issues:**
|
39
|
+
|
40
|
+
- JRuby Support [\#160](https://github.com/bensheldon/good_job/issues/160)
|
41
|
+
|
42
|
+
**Merged pull requests:**
|
43
|
+
|
44
|
+
- Update bundler version to 2.2.5 [\#200](https://github.com/bensheldon/good_job/pull/200) ([bensheldon](https://github.com/bensheldon))
|
45
|
+
- 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))
|
46
|
+
- Update GH Test Matrix with minimum & latest JRuby version [\#197](https://github.com/bensheldon/good_job/pull/197) ([tedhexaflow](https://github.com/tedhexaflow))
|
47
|
+
- Fix JRuby version number [\#193](https://github.com/bensheldon/good_job/pull/193) ([tedhexaflow](https://github.com/tedhexaflow))
|
48
|
+
|
49
|
+
## [v1.4.1](https://github.com/bensheldon/good_job/tree/v1.4.1) (2021-01-09)
|
50
|
+
|
51
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.4.0...v1.4.1)
|
52
|
+
|
53
|
+
**Fixed bugs:**
|
54
|
+
|
55
|
+
- Do not add lib/generators to Zeitwerk autoloader [\#192](https://github.com/bensheldon/good_job/pull/192) ([bensheldon](https://github.com/bensheldon))
|
56
|
+
|
57
|
+
**Closed issues:**
|
58
|
+
|
59
|
+
- Issues with Heroku and Good Job [\#184](https://github.com/bensheldon/good_job/issues/184)
|
60
|
+
|
61
|
+
**Merged pull requests:**
|
62
|
+
|
63
|
+
- Add missing YARD docs and Dashboard screenshot [\#191](https://github.com/bensheldon/good_job/pull/191) ([bensheldon](https://github.com/bensheldon))
|
64
|
+
|
65
|
+
## [v1.4.0](https://github.com/bensheldon/good_job/tree/v1.4.0) (2020-12-31)
|
66
|
+
|
67
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.6...v1.4.0)
|
68
|
+
|
69
|
+
**Implemented enhancements:**
|
70
|
+
|
71
|
+
- Add JRuby support [\#167](https://github.com/bensheldon/good_job/pull/167) ([bensheldon](https://github.com/bensheldon))
|
72
|
+
|
3
73
|
## [v1.3.6](https://github.com/bensheldon/good_job/tree/v1.3.6) (2020-12-30)
|
4
74
|
|
5
75
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.3.5...v1.3.6)
|
@@ -100,9 +170,6 @@
|
|
100
170
|
**Implemented enhancements:**
|
101
171
|
|
102
172
|
- Lengthen default poll interval from 1 to 5 seconds [\#156](https://github.com/bensheldon/good_job/pull/156) ([bensheldon](https://github.com/bensheldon))
|
103
|
-
|
104
|
-
**Merged pull requests:**
|
105
|
-
|
106
173
|
- 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))
|
107
174
|
|
108
175
|
## [v1.2.6](https://github.com/bensheldon/good_job/tree/v1.2.6) (2020-09-29)
|
@@ -212,11 +279,13 @@
|
|
212
279
|
**Implemented enhancements:**
|
213
280
|
|
214
281
|
- Run Github Action tests against Ruby 2.5, 2.6, 2.7 [\#100](https://github.com/bensheldon/good_job/issues/100)
|
282
|
+
- Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
|
215
283
|
|
216
284
|
**Fixed bugs:**
|
217
285
|
|
218
286
|
- Freezes puma on code change [\#95](https://github.com/bensheldon/good_job/issues/95)
|
219
287
|
- Ruby 2.7 keyword arguments warning [\#93](https://github.com/bensheldon/good_job/issues/93)
|
288
|
+
- Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
|
220
289
|
|
221
290
|
**Closed issues:**
|
222
291
|
|
@@ -226,10 +295,8 @@
|
|
226
295
|
|
227
296
|
- Use more ActiveRecord in Lockable and not connection.execute [\#102](https://github.com/bensheldon/good_job/pull/102) ([bensheldon](https://github.com/bensheldon))
|
228
297
|
- 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))
|
229
|
-
- Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
|
230
298
|
- Fix Ruby 2.7 keyword arguments warning [\#98](https://github.com/bensheldon/good_job/pull/98) ([arku](https://github.com/arku))
|
231
299
|
- Remove executor/reloader for less interlocking [\#97](https://github.com/bensheldon/good_job/pull/97) ([sj26](https://github.com/sj26))
|
232
|
-
- Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
|
233
300
|
- Add test for `rails g good\_job:install` [\#94](https://github.com/bensheldon/good_job/pull/94) ([arku](https://github.com/arku))
|
234
301
|
|
235
302
|
## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
|
data/README.md
CHANGED
@@ -30,12 +30,13 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
30
30
|
## Table of contents
|
31
31
|
|
32
32
|
- [Set up](#set-up)
|
33
|
+
- [Compatibility](#compatibility)
|
33
34
|
- [Configuration](#configuration)
|
34
35
|
- [Command-line options](#command-line-options)
|
35
36
|
- [`good_job start`](#good_job-start)
|
36
37
|
- [`good_job cleanup_preserved_jobs`](#good_job-cleanup_preserved_jobs)
|
37
|
-
- [
|
38
|
-
- [Global options](#global-options)
|
38
|
+
- [Configuration options](#configuration-options)
|
39
|
+
- [Global options](#global-options)pter
|
39
40
|
- [Dashboard](#dashboard)
|
40
41
|
- [Go deeper](#go-deeper)
|
41
42
|
- [Exceptions, retries, and reliability](#exceptions-retries-and-reliability)
|
@@ -123,6 +124,12 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
123
124
|
|
124
125
|
Additional configuration is likely necessary, see the reference below for async configuration.
|
125
126
|
|
127
|
+
## Compatibility
|
128
|
+
|
129
|
+
- **Ruby on Rails:** 5.2+
|
130
|
+
- **Ruby:** MRI 2.5+. JRuby 9.2.13+ (_JRuby's `activerecord-jdbcpostgresql-adapter` gem does not support Postgres LISTEN/NOTIFY)._
|
131
|
+
- **Postgres:** 9.6+
|
132
|
+
|
126
133
|
## Configuration
|
127
134
|
|
128
135
|
### Command-line options
|
@@ -145,6 +152,9 @@ Options:
|
|
145
152
|
[--max-threads=COUNT] # Maximum number of threads to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)
|
146
153
|
[--queues=QUEUE_LIST] # Queues to work from. (env var: GOOD_JOB_QUEUES, default: *)
|
147
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)
|
148
158
|
|
149
159
|
Executes queued jobs.
|
150
160
|
|
@@ -152,6 +162,7 @@ All options can be configured with environment variables.
|
|
152
162
|
See option descriptions for the matching environment variable name.
|
153
163
|
|
154
164
|
== Configuring queues
|
165
|
+
|
155
166
|
Separate multiple queues with commas; exclude queues with a leading minus;
|
156
167
|
separate isolated execution pools with semicolons and threads with colons.
|
157
168
|
```
|
@@ -182,9 +193,31 @@ If you are preserving job records this way, use this command regularly
|
|
182
193
|
to delete old records and preserve space in your database.
|
183
194
|
```
|
184
195
|
|
185
|
-
###
|
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
|
186
206
|
|
187
|
-
|
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
|
211
|
+
|
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:
|
188
221
|
|
189
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:
|
190
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.
|
@@ -193,18 +226,23 @@ To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job` o
|
|
193
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`.
|
194
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`.
|
195
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`.
|
196
230
|
|
197
|
-
|
231
|
+
By default, GoodJob configures the following execution modes per environment:
|
198
232
|
|
199
233
|
```ruby
|
234
|
+
|
200
235
|
# config/environments/development.rb
|
201
|
-
config.active_job.queue_adapter =
|
236
|
+
config.active_job.queue_adapter = :good_job
|
237
|
+
config.good_job.execution_mode = :inline
|
202
238
|
|
203
239
|
# config/environments/test.rb
|
204
|
-
config.active_job.queue_adapter =
|
240
|
+
config.active_job.queue_adapter = :good_job
|
241
|
+
config.good_job.execution_mode = :inline
|
205
242
|
|
206
243
|
# config/environments/production.rb
|
207
|
-
config.active_job.queue_adapter =
|
244
|
+
config.active_job.queue_adapter = :good_job
|
245
|
+
config.good_job.execution_mode = :external
|
208
246
|
```
|
209
247
|
|
210
248
|
### Global options
|
@@ -227,6 +265,8 @@ GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
|
|
227
265
|
|
228
266
|
### Dashboard
|
229
267
|
|
268
|
+

|
269
|
+
|
230
270
|
_🚧 GoodJob's dashboard is a work in progress. Please contribute ideas and code on [Github](https://github.com/bensheldon/good_job/issues)._
|
231
271
|
|
232
272
|
GoodJob includes a Dashboard as a mountable `Rails::Engine`.
|
@@ -433,14 +473,24 @@ pool: <%= [ENV.fetch("RAILS_MAX_THREADS", 5).to_i, ENV.fetch("GOOD_JOB_MAX_THREA
|
|
433
473
|
|
434
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:
|
435
475
|
|
436
|
-
-
|
476
|
+
- Via Rails configuration:
|
437
477
|
|
438
478
|
```ruby
|
439
479
|
# config/environments/production.rb
|
440
|
-
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
|
+
}
|
441
491
|
```
|
442
492
|
|
443
|
-
- Or,
|
493
|
+
- Or, with environment variables:
|
444
494
|
|
445
495
|
```bash
|
446
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
|
@@ -157,7 +157,8 @@ module GoodJob
|
|
157
157
|
SELECT 1 AS one
|
158
158
|
WHERE pg_try_advisory_lock(('x'||substr(md5($1 || $2::text), 1, 16))::bit(64)::bigint)
|
159
159
|
SQL
|
160
|
-
|
160
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
161
|
+
ActiveRecord::Base.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).any?
|
161
162
|
end
|
162
163
|
|
163
164
|
# Releases an advisory lock on this record if it is locked by this database
|
@@ -169,7 +170,8 @@ module GoodJob
|
|
169
170
|
SELECT 1 AS one
|
170
171
|
WHERE pg_advisory_unlock(('x'||substr(md5($1 || $2::text), 1, 16))::bit(64)::bigint)
|
171
172
|
SQL
|
172
|
-
|
173
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
174
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).any?
|
173
175
|
end
|
174
176
|
|
175
177
|
# Acquires an advisory lock on this record or raises
|
@@ -212,9 +214,10 @@ module GoodJob
|
|
212
214
|
WHERE pg_locks.locktype = 'advisory'
|
213
215
|
AND pg_locks.objsubid = 1
|
214
216
|
AND pg_locks.classid = ('x' || substr(md5($1 || $2::text), 1, 16))::bit(32)::int
|
215
|
-
AND pg_locks.objid = (('x' || substr(md5($
|
217
|
+
AND pg_locks.objid = (('x' || substr(md5($3 || $4::text), 1, 16))::bit(64) << 32)::bit(32)::int
|
216
218
|
SQL
|
217
|
-
self.class.
|
219
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)], [nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
220
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
|
218
221
|
end
|
219
222
|
|
220
223
|
# Tests whether this record is locked by the current database session.
|
@@ -226,10 +229,11 @@ module GoodJob
|
|
226
229
|
WHERE pg_locks.locktype = 'advisory'
|
227
230
|
AND pg_locks.objsubid = 1
|
228
231
|
AND pg_locks.classid = ('x' || substr(md5($1 || $2::text), 1, 16))::bit(32)::int
|
229
|
-
AND pg_locks.objid = (('x' || substr(md5($
|
232
|
+
AND pg_locks.objid = (('x' || substr(md5($3 || $4::text), 1, 16))::bit(64) << 32)::bit(32)::int
|
230
233
|
AND pg_locks.pid = pg_backend_pid()
|
231
234
|
SQL
|
232
|
-
self.class.
|
235
|
+
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)], [nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
236
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
|
233
237
|
end
|
234
238
|
|
235
239
|
# Releases all advisory locks on the record that are held by the current
|
@@ -245,5 +249,14 @@ module GoodJob
|
|
245
249
|
# Made public in Rails 5.2
|
246
250
|
self.class.send(:sanitize_sql_for_conditions, *args)
|
247
251
|
end
|
252
|
+
|
253
|
+
def pg_or_jdbc_query(query)
|
254
|
+
if Concurrent.on_jruby?
|
255
|
+
# Replace $1 bind parameters with ?
|
256
|
+
query.gsub(/\$\d*/, '?')
|
257
|
+
else
|
258
|
+
query
|
259
|
+
end
|
260
|
+
end
|
248
261
|
end
|
249
262
|
end
|
data/lib/good_job/notifier.rb
CHANGED
@@ -9,6 +9,9 @@ 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.
|
13
|
+
AdapterCannotListenError = Class.new(StandardError)
|
14
|
+
|
12
15
|
# Default Postgres channel for LISTEN/NOTIFY
|
13
16
|
CHANNEL = 'good_job'.freeze
|
14
17
|
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
@@ -94,6 +97,8 @@ module GoodJob # :nodoc:
|
|
94
97
|
# @!visibility private
|
95
98
|
# @return [void]
|
96
99
|
def listen_observer(_time, _result, thread_error)
|
100
|
+
return if thread_error.is_a? AdapterCannotListenError
|
101
|
+
|
97
102
|
if thread_error
|
98
103
|
GoodJob.on_thread_error.call(thread_error) if GoodJob.on_thread_error.respond_to?(:call)
|
99
104
|
ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: thread_error })
|
@@ -149,6 +154,8 @@ module GoodJob # :nodoc:
|
|
149
154
|
ActiveRecord::Base.connection_pool.remove(conn)
|
150
155
|
end
|
151
156
|
pg_conn = ar_conn.raw_connection
|
157
|
+
raise AdapterCannotListenError unless pg_conn.respond_to? :wait_for_notify
|
158
|
+
|
152
159
|
pg_conn.async_exec("SET application_name = #{pg_conn.escape_identifier(self.class.name)}").clear
|
153
160
|
yield pg_conn
|
154
161
|
ensure
|
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: 1, # ideally zero, but 0 == infinite
|
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,24 @@ 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
|
@pool_options[:max_threads] = max_threads if max_threads.present?
|
77
75
|
@pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]})"
|
78
76
|
|
79
77
|
create_pool
|
78
|
+
warm_cache if warm_cache_on_initialize
|
80
79
|
end
|
81
80
|
|
82
81
|
# Shut down the scheduler.
|
@@ -91,6 +90,8 @@ module GoodJob # :nodoc:
|
|
91
90
|
|
92
91
|
instrument("scheduler_shutdown_start", { wait: wait })
|
93
92
|
instrument("scheduler_shutdown", { wait: wait }) do
|
93
|
+
@timer_set.shutdown
|
94
|
+
|
94
95
|
@pool.shutdown
|
95
96
|
@pool.wait_for_termination if wait
|
96
97
|
# TODO: Should be killed if wait is not true
|
@@ -111,6 +112,7 @@ module GoodJob # :nodoc:
|
|
111
112
|
instrument("scheduler_restart_pools") do
|
112
113
|
shutdown(wait: wait) unless shutdown?
|
113
114
|
create_pool
|
115
|
+
warm_cache
|
114
116
|
end
|
115
117
|
end
|
116
118
|
|
@@ -121,10 +123,30 @@ module GoodJob # :nodoc:
|
|
121
123
|
# Returns +true+ if the performer started executing work.
|
122
124
|
# Returns +false+ if the performer decides not to attempt to execute a task based on the +state+ that is passed to it.
|
123
125
|
def create_thread(state = nil)
|
124
|
-
return nil unless @pool.running?
|
125
|
-
|
126
|
+
return nil unless @pool.running?
|
127
|
+
|
128
|
+
if state
|
129
|
+
return false unless @performer.next?(state)
|
130
|
+
|
131
|
+
if state[:scheduled_at]
|
132
|
+
scheduled_at = if state[:scheduled_at].is_a? String
|
133
|
+
Time.zone.parse state[:scheduled_at]
|
134
|
+
else
|
135
|
+
state[:scheduled_at]
|
136
|
+
end
|
137
|
+
delay = [(scheduled_at - Time.current).to_f, 0].max
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
delay ||= 0
|
142
|
+
run_now = delay <= 0.01
|
143
|
+
if run_now
|
144
|
+
return nil unless @pool.ready_worker_count.positive?
|
145
|
+
elsif @max_cache.positive?
|
146
|
+
return nil unless remaining_cache_count.positive?
|
147
|
+
end
|
126
148
|
|
127
|
-
future = Concurrent::
|
149
|
+
future = Concurrent::ScheduledTask.new(delay, args: [@performer], executor: @pool, timer_set: timer_set) do |performer|
|
128
150
|
output = nil
|
129
151
|
Rails.application.executor.wrap { output = performer.next }
|
130
152
|
output
|
@@ -132,7 +154,7 @@ module GoodJob # :nodoc:
|
|
132
154
|
future.add_observer(self, :task_observer)
|
133
155
|
future.execute
|
134
156
|
|
135
|
-
true
|
157
|
+
run_now ? true : nil
|
136
158
|
end
|
137
159
|
|
138
160
|
# Invoked on completion of ThreadPoolExecutor task
|
@@ -144,10 +166,36 @@ module GoodJob # :nodoc:
|
|
144
166
|
create_thread if output
|
145
167
|
end
|
146
168
|
|
169
|
+
def warm_cache
|
170
|
+
return if @max_cache.zero?
|
171
|
+
|
172
|
+
@performer.next_at(
|
173
|
+
limit: @max_cache,
|
174
|
+
now_limit: @pool_options[:max_threads]
|
175
|
+
).each do |scheduled_at|
|
176
|
+
create_thread({ scheduled_at: scheduled_at })
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def stats
|
181
|
+
{
|
182
|
+
name: @performer.name,
|
183
|
+
max_threads: @pool_options[:max_threads],
|
184
|
+
active_threads: @pool.ready_worker_count - @pool_options[:max_threads],
|
185
|
+
inactive_threads: @pool.ready_worker_count,
|
186
|
+
max_cache: @max_cache,
|
187
|
+
cache_count: cache_count,
|
188
|
+
cache_remaining: remaining_cache_count,
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
147
192
|
private
|
148
193
|
|
194
|
+
attr_reader :timer_set
|
195
|
+
|
149
196
|
def create_pool
|
150
197
|
instrument("scheduler_create_pool", { performer_name: @performer.name, max_threads: @pool_options[:max_threads] }) do
|
198
|
+
@timer_set = Concurrent::TimerSet.new
|
151
199
|
@pool = ThreadPoolExecutor.new(@pool_options)
|
152
200
|
end
|
153
201
|
end
|
@@ -162,6 +210,14 @@ module GoodJob # :nodoc:
|
|
162
210
|
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
163
211
|
end
|
164
212
|
|
213
|
+
def cache_count
|
214
|
+
timer_set.instance_variable_get(:@queue).length
|
215
|
+
end
|
216
|
+
|
217
|
+
def remaining_cache_count
|
218
|
+
@max_cache - cache_count
|
219
|
+
end
|
220
|
+
|
165
221
|
# Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
|
166
222
|
# @private
|
167
223
|
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
@@ -170,10 +226,13 @@ module GoodJob # :nodoc:
|
|
170
226
|
# @return [Integer]
|
171
227
|
def ready_worker_count
|
172
228
|
synchronize do
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
229
|
+
if Concurrent.on_jruby?
|
230
|
+
@executor.getMaximumPoolSize - @executor.getActiveCount
|
231
|
+
else
|
232
|
+
workers_still_to_be_created = @max_length - @pool.length
|
233
|
+
workers_created_but_waiting = @ready.length
|
234
|
+
workers_still_to_be_created + workers_created_but_waiting
|
235
|
+
end
|
177
236
|
end
|
178
237
|
end
|
179
238
|
end
|
data/lib/good_job/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
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-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -52,20 +52,6 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 1.0.2
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: pg
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 1.0.0
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 1.0.0
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: railties
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -262,34 +248,6 @@ dependencies:
|
|
262
248
|
- - ">="
|
263
249
|
- !ruby/object:Gem::Version
|
264
250
|
version: '0'
|
265
|
-
- !ruby/object:Gem::Dependency
|
266
|
-
name: rails
|
267
|
-
requirement: !ruby/object:Gem::Requirement
|
268
|
-
requirements:
|
269
|
-
- - ">="
|
270
|
-
- !ruby/object:Gem::Version
|
271
|
-
version: '0'
|
272
|
-
type: :development
|
273
|
-
prerelease: false
|
274
|
-
version_requirements: !ruby/object:Gem::Requirement
|
275
|
-
requirements:
|
276
|
-
- - ">="
|
277
|
-
- !ruby/object:Gem::Version
|
278
|
-
version: '0'
|
279
|
-
- !ruby/object:Gem::Dependency
|
280
|
-
name: rbtrace
|
281
|
-
requirement: !ruby/object:Gem::Requirement
|
282
|
-
requirements:
|
283
|
-
- - ">="
|
284
|
-
- !ruby/object:Gem::Version
|
285
|
-
version: '0'
|
286
|
-
type: :development
|
287
|
-
prerelease: false
|
288
|
-
version_requirements: !ruby/object:Gem::Requirement
|
289
|
-
requirements:
|
290
|
-
- - ">="
|
291
|
-
- !ruby/object:Gem::Version
|
292
|
-
version: '0'
|
293
251
|
- !ruby/object:Gem::Dependency
|
294
252
|
name: rspec-rails
|
295
253
|
requirement: !ruby/object:Gem::Requirement
|
@@ -399,12 +357,13 @@ files:
|
|
399
357
|
- lib/good_job/cli.rb
|
400
358
|
- lib/good_job/configuration.rb
|
401
359
|
- lib/good_job/current_execution.rb
|
360
|
+
- lib/good_job/daemon.rb
|
402
361
|
- lib/good_job/job.rb
|
362
|
+
- lib/good_job/job_performer.rb
|
403
363
|
- lib/good_job/lockable.rb
|
404
364
|
- lib/good_job/log_subscriber.rb
|
405
365
|
- lib/good_job/multi_scheduler.rb
|
406
366
|
- lib/good_job/notifier.rb
|
407
|
-
- lib/good_job/performer.rb
|
408
367
|
- lib/good_job/poller.rb
|
409
368
|
- lib/good_job/railtie.rb
|
410
369
|
- lib/good_job/scheduler.rb
|
@@ -441,7 +400,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
441
400
|
- !ruby/object:Gem::Version
|
442
401
|
version: '0'
|
443
402
|
requirements: []
|
444
|
-
rubygems_version: 3.2.
|
403
|
+
rubygems_version: 3.2.4
|
445
404
|
signing_key:
|
446
405
|
specification_version: 4
|
447
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
|