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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a970811c9e009e460f65ed53b23913ae530e2a4018811ed63aa382ac673ad8a
4
- data.tar.gz: 5678206af710fdbc275486529451356ae7ab8b2f52c64e8743db4789c806319d
3
+ metadata.gz: 86268c6767ba44783707ed768fe726b7a53592e46468ed49bd38d427ff4919af
4
+ data.tar.gz: 5fa72a4aaec59a31ba437eac31f9a6785cdb548c6694bea332434baf58aaad98
5
5
  SHA512:
6
- metadata.gz: 5a1cc256e46120045b1c5f9583c9c9f6ee948a60b8e3a0f4a0dd6ee74be411dbe3d8303c6b8c0c536804f563b8819174a3840814a66f4e4110ba25244a25cffb
7
- data.tar.gz: 11224d38b15c50108e14e529f645de8e9991aeb265414fea4513aaa253da6c13fcc9b981132516497db53f185430bb1b775ee1f5964cd8d9ece528a9a5353f70
6
+ metadata.gz: c63baa745a861101b1a65c7a2dd7f4ec603a50714d5952a7ad942f30b41543643e6b7e1f19b1373f280668f212c6e173c4aa90d7d3a0770a92c1e159ae30215b
7
+ data.tar.gz: 7bf564c4cfc0804f1a32995214195736e9cbd8d8f7a5b45d3ffe7efecf3d43126d412594855d50c9637c646e82838112a4869a4732fa0fe5ba1542b558facd0f
@@ -1,17 +1,88 @@
1
1
  # Changelog
2
2
 
3
- ## [v1.4.0](https://github.com/bensheldon/good_job/tree/v1.4.0) (2020-12-31)
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.3.6...v1.4.0)
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
- - Add JRuby support [\#167](https://github.com/bensheldon/good_job/pull/167) ([bensheldon](https://github.com/bensheldon))
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
- - [Adapter options](#adapter-options)
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
- ### Adapter options
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
- To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job` or to an instance of `GoodJob::Adapter`, which you can configure with several options:
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
- Using the symbol instead of explicitly configuring the options above (i.e. setting `config.active_job.queue_adapter = :good_job`) is equivalent to:
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 = GoodJob::Adapter.new(execution_mode: :inline)
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 = GoodJob::Adapter.new(execution_mode: :inline)
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 = GoodJob::Adapter.new(execution_mode: :external)
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
+ ![Dashboard UI](https://github.com/bensheldon/good_job/raw/main/SCREENSHOT.png)
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
- - Directly configure the ActiveJob adapter:
476
+ - Via Rails configuration:
444
477
 
445
478
  ```ruby
446
479
  # config/environments/production.rb
447
- config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :async, max_threads: 4, poll_interval: 30)
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, when using `...queue_adapter = :good_job`, via environment variables:
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'good_job/cli'
3
+ GOOD_JOB_WITHIN_CLI = true
3
4
  GOOD_JOB_LOG_TO_STDOUT = true
4
5
  GoodJob::CLI.start(ARGV)
@@ -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(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
6
- configuration = GoodJob::Configuration.new({ execution_mode: execution_mode }, env: ENV)
7
- super(execution_mode: configuration.rails_execution_mode, max_threads: max_threads, poll_interval: poll_interval, scheduler: scheduler, inline: inline)
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
@@ -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 = Zeitwerk::Loader.for_gem
9
- loader.inflector.inflect(
10
- 'cli' => "CLI"
11
- )
12
- loader.push_dir(File.join(__dir__, ["generators"]))
13
- loader.setup
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
 
@@ -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
- # @param scheduler [nil, Scheduler] (deprecated) a scheduler to be managed by the adapter
24
- # @param notifier [nil, Notifier] (deprecated) a notifier to be managed by the adapter
25
- # @param inline [nil, Boolean] (deprecated) whether to run in inline execution mode
26
- def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, scheduler: nil, notifier: nil, inline: false)
27
- if inline && execution_mode.nil?
28
- ActiveSupport::Deprecation.warn('GoodJob::Adapter#new(inline: true) is deprecated; use GoodJob::Adapter.new(execution_mode: :inline) instead')
29
- execution_mode = :inline
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 @execution_mode == :async # rubocop:disable Style/GuardClause
45
- @notifier = notifier || GoodJob::Notifier.new
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 = scheduler || GoodJob::Scheduler.from_configuration(configuration)
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
- end
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
- executed_locally = execute_async? && @scheduler.create_thread(queue_name: good_job.queue_name)
82
- Notifier.notify(queue_name: good_job.queue_name) unless executed_locally
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
@@ -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
- \x5Separate multiple queues with commas; exclude queues with a leading minus;
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 = 5
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
- # @!attribute [r] options
16
- # The options that were explicitly set when initializing +Configuration+.
17
- # @return [Hash]
18
- #
19
- # @!attribute [r] env
20
- # The environment from which to read GoodJob's environment variables. By
21
- # default, this is the current process's environment, but it can be set
22
- # to something else in {#initialize}.
23
- # @return [Hash]
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 options[:execution_mode]
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,8 +1,12 @@
1
1
  module GoodJob
2
2
  # Ruby on Rails integration.
3
3
  class Railtie < ::Rails::Railtie
4
- initializer "good_job.logger" do
5
- ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
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
@@ -1,5 +1,6 @@
1
1
  require "concurrent/executor/thread_pool_executor"
2
- require "concurrent/timer_task"
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 {Performer} that will execute tasks.
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: 0,
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
- job_query = GoodJob::Job.queue_string(queue_string)
44
- parsed = GoodJob::Job.queue_parser(queue_string)
45
- job_filter = proc do |state|
46
- if parsed[:exclude]
47
- parsed[:exclude].exclude?(state[:queue_name])
48
- elsif parsed[:include]
49
- parsed[:include].include? state[:queue_name]
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::Performer]
61
+ # @param performer [GoodJob::JobPerformer]
67
62
  # @param max_threads [Numeric, nil] number of seconds between polls for jobs
68
- def initialize(performer, max_threads: nil)
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
- @pool_options[:max_threads] = max_threads if max_threads.present?
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? && @pool.ready_worker_count.positive?
125
- return false if state && !@performer.next?(state)
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
- future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
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
- create_thread if output
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
@@ -1,4 +1,4 @@
1
1
  module GoodJob
2
2
  # GoodJob gem version.
3
- VERSION = '1.4.0'.freeze
3
+ VERSION = '1.7.1'.freeze
4
4
  end
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.0
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: 2020-12-31 00:00:00.000000000 Z
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.3
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
@@ -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