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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b5d625792328475faca2679c6718f344fc5162cac639be09f91197c998f2fdf
4
- data.tar.gz: 4aef2c4ae814f2a0606c18e875c91e56f8a1411394c37d45f4e1141be719f080
3
+ metadata.gz: ba54d04d3afa9fea7af913fb6e04f3bdbc104f47a57b629001071da7fcd4ed55
4
+ data.tar.gz: 2bd4dd5be31a15c43d58f0ab7cd33830834e1e2bcd0506445258aa75d4cc98e5
5
5
  SHA512:
6
- metadata.gz: d309e288c89c65ec809cf7f9e138026b229aed9abb6ee667343c56ed1e96fb0833be36b7e1f1c6b064168957f537d572e2ceb5ecad0f6e754108762ace9fbd3b
7
- data.tar.gz: c1b83122c14edcbe619c6198fae270f9dbb0398888232f96e8956be23ca0f5798422181a21ea08735a0089ad3a92c0405ee69731a8a205f3fe9c99ba2b27bb36
6
+ metadata.gz: 8ade9720ef3918e10d129e886411b819e8fd96614a299552d68287ed43fcab2997057b8c63ee26b35ac321cf7d58a055c7cf5b14f74f3c887b14f59934537be8
7
+ data.tar.gz: 9f81f5e7faacbe1b6a4999fa82afa6eb03675472c0237616f6f570c235f72c8c925fbea0ff526726bb6542c795ef47feaa61b5e9dd00520d6a38fbfa4b7463a5
@@ -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
- - [Adapter options](#adapter-options)
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
- ### 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
186
206
 
187
- 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:
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
- 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:
198
232
 
199
233
  ```ruby
234
+
200
235
  # config/environments/development.rb
201
- 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
202
238
 
203
239
  # config/environments/test.rb
204
- 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
205
242
 
206
243
  # config/environments/production.rb
207
- 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
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
+ ![Dashboard UI](https://github.com/bensheldon/good_job/raw/main/SCREENSHOT.png)
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
- - Directly configure the ActiveJob adapter:
476
+ - Via Rails configuration:
437
477
 
438
478
  ```ruby
439
479
  # config/environments/production.rb
440
- 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
+ }
441
491
  ```
442
492
 
443
- - Or, when using `...queue_adapter = :good_job`, via environment variables:
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
@@ -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
@@ -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
- self.class.connection.exec_query(query, 'GoodJob::Lockable Advisory Lock', [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]).any?
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
- self.class.connection.exec_query(query, 'GoodJob::Lockable Advisory Unlock', [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]).any?
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($1 || $2::text), 1, 16))::bit(64) << 32)::bit(32)::int
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.connection.exec_query(query, 'GoodJob::Lockable Advisory Locked?', [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]).any?
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($1 || $2::text), 1, 16))::bit(64) << 32)::bit(32)::int
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.connection.exec_query(query, 'GoodJob::Lockable Owns Advisory Lock?', [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]).any?
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
@@ -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
@@ -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: -1,
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
- 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,24 @@ 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
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? && @pool.ready_worker_count.positive?
125
- return false if state && !@performer.next?(state)
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::Future.new(args: [@performer], executor: @pool) do |performer|
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
- workers_still_to_be_created = @max_length - @pool.length
174
- workers_created_but_waiting = @ready.length
175
-
176
- workers_still_to_be_created + workers_created_but_waiting
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
@@ -1,4 +1,4 @@
1
1
  module GoodJob
2
2
  # GoodJob gem version.
3
- VERSION = '1.3.6'.freeze
3
+ VERSION = '1.7.0'.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.3.6
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: 2020-12-30 00:00:00.000000000 Z
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.3
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
@@ -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