good_job 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -2
- data/README.md +47 -3
- data/engine/app/controllers/good_job/active_jobs_controller.rb +1 -0
- data/engine/app/controllers/good_job/assets_controller.rb +1 -0
- data/engine/app/controllers/good_job/base_controller.rb +1 -0
- data/engine/app/controllers/good_job/dashboards_controller.rb +1 -0
- data/engine/app/controllers/good_job/jobs_controller.rb +1 -0
- data/engine/app/helpers/good_job/application_helper.rb +1 -0
- data/engine/app/views/good_job/active_jobs/show.html.erb +1 -1
- data/engine/app/views/good_job/dashboards/index.html.erb +2 -2
- data/engine/app/views/{shared → good_job/shared}/_chart.erb +2 -2
- data/engine/app/views/{shared → good_job/shared}/_jobs_table.erb +1 -1
- data/engine/app/views/{shared → good_job/shared}/icons/_check.html.erb +0 -0
- data/engine/app/views/{shared → good_job/shared}/icons/_exclamation.html.erb +0 -0
- data/engine/app/views/{shared → good_job/shared}/icons/_trash.html.erb +0 -0
- data/engine/app/views/layouts/good_job/base.html.erb +7 -7
- data/engine/config/routes.rb +11 -5
- data/engine/lib/good_job/engine.rb +1 -0
- data/exe/good_job +1 -0
- data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
- data/lib/generators/good_job/install_generator.rb +1 -0
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb +1 -0
- data/lib/generators/good_job/templates/update/migrations/02_add_active_job_id_concurrency_key_cron_key_to_good_jobs.rb +1 -0
- data/lib/generators/good_job/templates/update/migrations/03_add_active_job_id_index_and_concurrency_key_index_to_good_jobs.rb +1 -0
- data/lib/generators/good_job/update_generator.rb +1 -0
- data/lib/good_job.rb +13 -7
- data/lib/good_job/active_job_extensions.rb +1 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +1 -0
- data/lib/good_job/adapter.rb +3 -0
- data/lib/good_job/cli.rb +8 -2
- data/lib/good_job/configuration.rb +25 -0
- data/lib/good_job/cron_manager.rb +115 -0
- data/lib/good_job/current_execution.rb +7 -0
- data/lib/good_job/daemon.rb +1 -0
- data/lib/good_job/execution_result.rb +1 -0
- data/lib/good_job/job.rb +25 -2
- data/lib/good_job/job_performer.rb +1 -0
- data/lib/good_job/lockable.rb +22 -15
- data/lib/good_job/log_subscriber.rb +11 -0
- data/lib/good_job/multi_scheduler.rb +1 -0
- data/lib/good_job/notifier.rb +13 -4
- data/lib/good_job/poller.rb +1 -0
- data/lib/good_job/railtie.rb +3 -0
- data/lib/good_job/scheduler.rb +1 -0
- data/lib/good_job/version.rb +2 -1
- metadata +22 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bdf4b1eebb1224acd53f07753d5cda9cb9fde7a44baf300824d7816da94980f2
|
4
|
+
data.tar.gz: 4b90c9cd8133f179f87094683218ed1a1aed3582512169dd914a00dd62a81628
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e95350cb8bd69198231d8030cc07adb9c87649499d30b6aa467ad4a06244920f2e950f46eae891e6963532fa879000959fcaa2eddf6093b8cd9d960eefef9abb
|
7
|
+
data.tar.gz: 1e09cf2851ca64fc34407e29f5afbac5bd7a72f072cef7c22798458b9ebc5678e287abbb03ee0d070cea8aa0665d85b017b94b68737e3e26ce7c0f67905c66e0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,65 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.12.0](https://github.com/bensheldon/good_job/tree/v1.12.0) (2021-07-27)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.11.3...v1.12.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Add the ability to schedule repeating / recurring / cron-like jobs [\#53](https://github.com/bensheldon/good_job/issues/53)
|
10
|
+
- Add cron-like support for recurring/repeating jobs [\#297](https://github.com/bensheldon/good_job/pull/297) ([bensheldon](https://github.com/bensheldon))
|
11
|
+
|
12
|
+
**Merged pull requests:**
|
13
|
+
|
14
|
+
- Place Dashboard shared view partials under `good_job` namespace [\#310](https://github.com/bensheldon/good_job/pull/310) ([bensheldon](https://github.com/bensheldon))
|
15
|
+
- Ensure Dashboard inline javascript has CSP nonce for strict Content-Security Policy [\#309](https://github.com/bensheldon/good_job/pull/309) ([bensheldon](https://github.com/bensheldon))
|
16
|
+
|
17
|
+
## [v1.11.3](https://github.com/bensheldon/good_job/tree/v1.11.3) (2021-07-25)
|
18
|
+
|
19
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.11.2...v1.11.3)
|
20
|
+
|
21
|
+
**Closed issues:**
|
22
|
+
|
23
|
+
- ERROR: relation "good\_jobs" does not exist at character 454 [\#308](https://github.com/bensheldon/good_job/issues/308)
|
24
|
+
- Add Frozen String Literal to all files [\#298](https://github.com/bensheldon/good_job/issues/298)
|
25
|
+
- Support for good\_job without Rails? [\#295](https://github.com/bensheldon/good_job/issues/295)
|
26
|
+
|
27
|
+
**Merged pull requests:**
|
28
|
+
|
29
|
+
- Have prettier Dashboard asset urls e.g. `bootstrap.css` instead of `bootstrap_css.css` [\#306](https://github.com/bensheldon/good_job/pull/306) ([bensheldon](https://github.com/bensheldon))
|
30
|
+
- Create dashboard demo app on Heroku [\#305](https://github.com/bensheldon/good_job/pull/305) ([bensheldon](https://github.com/bensheldon))
|
31
|
+
- Add Frozen String Literal to all files [\#302](https://github.com/bensheldon/good_job/pull/302) ([tedhexaflow](https://github.com/tedhexaflow))
|
32
|
+
|
33
|
+
## [v1.11.2](https://github.com/bensheldon/good_job/tree/v1.11.2) (2021-07-20)
|
34
|
+
|
35
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.11.1...v1.11.2)
|
36
|
+
|
37
|
+
**Fixed bugs:**
|
38
|
+
|
39
|
+
- Notifier waits to retry listening when database is unavailable [\#301](https://github.com/bensheldon/good_job/pull/301) ([bensheldon](https://github.com/bensheldon))
|
40
|
+
|
41
|
+
**Closed issues:**
|
42
|
+
|
43
|
+
- Handle database connection drops [\#296](https://github.com/bensheldon/good_job/issues/296)
|
44
|
+
- Using the `async` worker results in `ActiveModel::UnknownAttributeError unknown attribute 'create_with_advisory_lock' for GoodJob::Job`. [\#290](https://github.com/bensheldon/good_job/issues/290)
|
45
|
+
|
46
|
+
**Merged pull requests:**
|
47
|
+
|
48
|
+
- Rename development and test databases to be `good_job` [\#300](https://github.com/bensheldon/good_job/pull/300) ([bensheldon](https://github.com/bensheldon))
|
49
|
+
- Move generators spec into top-level spec directory; update dependencies [\#299](https://github.com/bensheldon/good_job/pull/299) ([bensheldon](https://github.com/bensheldon))
|
50
|
+
|
51
|
+
## [v1.11.1](https://github.com/bensheldon/good_job/tree/v1.11.1) (2021-07-07)
|
52
|
+
|
53
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.11.0...v1.11.1)
|
54
|
+
|
55
|
+
**Fixed bugs:**
|
56
|
+
|
57
|
+
- Defer accessing ActiveRecord `primary_key` in Lockable [\#293](https://github.com/bensheldon/good_job/pull/293) ([bensheldon](https://github.com/bensheldon))
|
58
|
+
|
59
|
+
**Closed issues:**
|
60
|
+
|
61
|
+
- Database connection required while loading the code on 1.10.x [\#291](https://github.com/bensheldon/good_job/issues/291)
|
62
|
+
|
3
63
|
## [v1.11.0](https://github.com/bensheldon/good_job/tree/v1.11.0) (2021-07-07)
|
4
64
|
|
5
65
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.10.1...v1.11.0)
|
@@ -20,9 +80,12 @@
|
|
20
80
|
|
21
81
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.10.0...v1.10.1)
|
22
82
|
|
23
|
-
**
|
83
|
+
**Fixed bugs:**
|
24
84
|
|
25
85
|
- Remove `FOR UPDATE SKIP LOCKED` from job locking sql statement [\#288](https://github.com/bensheldon/good_job/pull/288) ([bensheldon](https://github.com/bensheldon))
|
86
|
+
|
87
|
+
**Merged pull requests:**
|
88
|
+
|
26
89
|
- Update GH Test Matrix with latest JRuby 9.2.19.0 [\#283](https://github.com/bensheldon/good_job/pull/283) ([tedhexaflow](https://github.com/tedhexaflow))
|
27
90
|
|
28
91
|
## [v1.10.0](https://github.com/bensheldon/good_job/tree/v1.10.0) (2021-06-29)
|
@@ -31,11 +94,11 @@
|
|
31
94
|
|
32
95
|
**Implemented enhancements:**
|
33
96
|
|
97
|
+
- Use `pg_advisory_unlock_all` after each thread's job execution; fix Lockable return values; improve test stability [\#285](https://github.com/bensheldon/good_job/pull/285) ([bensheldon](https://github.com/bensheldon))
|
34
98
|
- Add `rails g good_job:update` command to add idempotent migration files, including `active_job_id`, `concurrency_key`, `cron_key` columns [\#266](https://github.com/bensheldon/good_job/pull/266) ([bensheldon](https://github.com/bensheldon))
|
35
99
|
|
36
100
|
**Fixed bugs:**
|
37
101
|
|
38
|
-
- Use `pg_advisory_unlock_all` after each thread's job execution; fix Lockable return values; improve test stability [\#285](https://github.com/bensheldon/good_job/pull/285) ([bensheldon](https://github.com/bensheldon))
|
39
102
|
- Dashboard AssetsController does not raise if verify\_authenticity\_token is not in the callback chain [\#284](https://github.com/bensheldon/good_job/pull/284) ([bensheldon](https://github.com/bensheldon))
|
40
103
|
|
41
104
|
**Closed issues:**
|
data/README.md
CHANGED
@@ -38,7 +38,8 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
38
38
|
- [Configuration options](#configuration-options)
|
39
39
|
- [Global options](#global-options)
|
40
40
|
- [Dashboard](#dashboard)
|
41
|
-
- [ActiveJob
|
41
|
+
- [ActiveJob concurrency](#activejob-concurrency)
|
42
|
+
- [Cron-style repeating/recurring jobs](#cron-style-repeatingrecurring-jobs)
|
42
43
|
- [Updating](#updating)
|
43
44
|
- [Go deeper](#go-deeper)
|
44
45
|
- [Exceptions, retries, and reliability](#exceptions-retries-and-reliability)
|
@@ -156,6 +157,7 @@ Options:
|
|
156
157
|
[--poll-interval=SECONDS] # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)
|
157
158
|
[--max-cache=COUNT] # Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)
|
158
159
|
[--shutdown-timeout=SECONDS] # Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))
|
160
|
+
[--enable-cron] # Whether to run cron process (default: false)
|
159
161
|
[--daemonize] # Run as a background daemon (default: false)
|
160
162
|
[--pidfile=PIDFILE] # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)
|
161
163
|
|
@@ -212,7 +214,8 @@ config.good_job.execution_mode = :async_server
|
|
212
214
|
config.good_job.max_threads = 5
|
213
215
|
config.good_job.poll_interval = 30 # seconds
|
214
216
|
config.good_job.shutdown_timeout = 25 # seconds
|
215
|
-
|
217
|
+
config.good_job.enable_cron = true
|
218
|
+
config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob' } }
|
216
219
|
|
217
220
|
# ...or all at once.
|
218
221
|
config.good_job = {
|
@@ -220,6 +223,13 @@ config.good_job = {
|
|
220
223
|
max_threads: 5,
|
221
224
|
poll_interval: 30,
|
222
225
|
shutdown_timeout: 25,
|
226
|
+
enable_cron: true,
|
227
|
+
cron: {
|
228
|
+
example: {
|
229
|
+
cron: '0 * * * *',
|
230
|
+
class: 'ExampleJob'
|
231
|
+
},
|
232
|
+
},
|
223
233
|
}
|
224
234
|
```
|
225
235
|
|
@@ -235,6 +245,8 @@ Available configuration options are:
|
|
235
245
|
- `poll_interval` (integer) sets the number of seconds between polls for jobs when `execution_mode` is set to `:async` or `:async_server`. You can also set this with the environment variable `GOOD_JOB_POLL_INTERVAL`.
|
236
246
|
- `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`.
|
237
247
|
- `shutdown_timeout` (float) number of seconds to wait for jobs to finish when shutting down before stopping the thread. Defaults to forever: `-1`. You can also set this with the environment variable `GOOD_JOB_SHUTDOWN_TIMEOUT`.
|
248
|
+
- `enable_cron` (boolean) whether to run cron process. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_CRON`.
|
249
|
+
- `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
|
238
250
|
|
239
251
|
By default, GoodJob configures the following execution modes per environment:
|
240
252
|
|
@@ -320,7 +332,7 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
|
|
320
332
|
end
|
321
333
|
```
|
322
334
|
|
323
|
-
### ActiveJob
|
335
|
+
### ActiveJob concurrency
|
324
336
|
|
325
337
|
GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_.
|
326
338
|
|
@@ -349,6 +361,38 @@ class MyJob < ApplicationJob
|
|
349
361
|
end
|
350
362
|
```
|
351
363
|
|
364
|
+
### Cron-style repeating/recurring jobs
|
365
|
+
|
366
|
+
GoodJob can enqueue jobs on a recurring basis that can be used as a replacement for cron.
|
367
|
+
|
368
|
+
Cron-style jobs are run on every GoodJob process (e.g. CLI or `async` execution mode) when `config.good_job.enable_cron = true`; use GoodJob's [ActiveJob concurrency](#activejob-concurrency) extension to limit the number of jobs that are enqueued.
|
369
|
+
|
370
|
+
Cron-format is parsed by the [`fugit`](https://github.com/floraison/fugit) gem, which has support for seconds-level resolution (e.g. `* * * * * *`).
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
# config/environments/application.rb or a specific environment e.g. production.rb
|
374
|
+
|
375
|
+
# Enable cron in this process; e.g. only run on the first Heroku worker process
|
376
|
+
config.good_job.enable_cron = ENV['DYNO'] == 'worker.1' # or `true` or via $GOOD_JOB_ENABLE_CRON
|
377
|
+
|
378
|
+
# Configure cron with a hash that has a unique key for each recurring job
|
379
|
+
config.good_job.cron = {
|
380
|
+
# Every 15 minutes, enqueue `ExampleJob.set(priority: -10).perform_later(52, name: "Alice")`
|
381
|
+
frequent_task: { # each recurring job must have a unique key
|
382
|
+
cron: "*/15 * * * *", # cron-style scheduling format by fugit gem
|
383
|
+
class: "ExampleJob", # reference the Job class with a string
|
384
|
+
args: [42, { name: "Alice" }], # arguments to pass; can also be a proc e.g. `-> { { when: Time.now } }`
|
385
|
+
set: { priority: -10 }, # additional ActiveJob properties; can also be a lambda/proc e.g. `-> { { priority: [1,2].sample } }`
|
386
|
+
description: "Something helpful", # optional description that appears in Dashboard (coming soon!)
|
387
|
+
},
|
388
|
+
another_task: {
|
389
|
+
cron: "0 0,12 * * *",
|
390
|
+
class: "AnotherJob",
|
391
|
+
},
|
392
|
+
# etc.
|
393
|
+
}
|
394
|
+
```
|
395
|
+
|
352
396
|
### Updating
|
353
397
|
|
354
398
|
GoodJob follows semantic versioning, though updates may be encouraged through deprecation warnings in minor versions.
|
@@ -1 +1 @@
|
|
1
|
-
<%= render 'shared/jobs_table', jobs: @jobs %>
|
1
|
+
<%= render 'good_job/shared/jobs_table', jobs: @jobs %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="card my-3 p-6">
|
2
|
-
<%= render 'shared/chart', chart_data: @chart %>
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: @chart %>
|
3
3
|
</div>
|
4
4
|
|
5
5
|
<div class='card mb-2'>
|
@@ -38,7 +38,7 @@
|
|
38
38
|
</div>
|
39
39
|
|
40
40
|
<% if @filter.jobs.present? %>
|
41
|
-
<%= render 'shared/jobs_table', jobs: @filter.jobs %>
|
41
|
+
<%= render 'good_job/shared/jobs_table', jobs: @filter.jobs %>
|
42
42
|
|
43
43
|
<nav aria-label="Job pagination" class="mt-3">
|
44
44
|
<ul class="pagination">
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<div id="chart"></div>
|
2
2
|
|
3
|
-
|
3
|
+
<%= javascript_tag nonce: true do %>
|
4
4
|
new Chartist.Line('#chart', <%== chart_data.to_json %>, {
|
5
5
|
height: '300px',
|
6
6
|
fullWidth: true,
|
@@ -49,4 +49,4 @@
|
|
49
49
|
tooltipEl.style.left = (event.offsetX || event.originalEvent.layerX) + tooltipEl.offsetWidth + 10 + 'px';
|
50
50
|
tooltipEl.style.top = (event.offsetY || event.originalEvent.layerY) + tooltipEl.offsetHeight - 20 + 'px';
|
51
51
|
}, true);
|
52
|
-
|
52
|
+
<% end %>
|
File without changes
|
File without changes
|
File without changes
|
@@ -5,12 +5,12 @@
|
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
7
|
|
8
|
-
<%= stylesheet_link_tag
|
9
|
-
<%= stylesheet_link_tag
|
10
|
-
<%= stylesheet_link_tag
|
8
|
+
<%= stylesheet_link_tag bootstrap_path(format: :css, v: GoodJob::VERSION) %>
|
9
|
+
<%= stylesheet_link_tag chartist_path(format: :css, v: GoodJob::VERSION) %>
|
10
|
+
<%= stylesheet_link_tag style_path(format: :css, v: GoodJob::VERSION) %>
|
11
11
|
|
12
|
-
<%= javascript_include_tag
|
13
|
-
<%= javascript_include_tag
|
12
|
+
<%= javascript_include_tag bootstrap_path(format: :js, v: GoodJob::VERSION) %>
|
13
|
+
<%= javascript_include_tag chartist_path(format: :js, v: GoodJob::VERSION) %>
|
14
14
|
</head>
|
15
15
|
<body>
|
16
16
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
@@ -53,13 +53,13 @@
|
|
53
53
|
|
54
54
|
<% if notice %>
|
55
55
|
<div class="alert alert-success alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
|
56
|
-
<%= render "shared/icons/check", class: "flex-shrink-0 me-2" %>
|
56
|
+
<%= render "good_job/shared/icons/check", class: "flex-shrink-0 me-2" %>
|
57
57
|
<div><%= notice %></div>
|
58
58
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
59
59
|
</div>
|
60
60
|
<% elsif alert %>
|
61
61
|
<div class="alert alert-warning alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
|
62
|
-
<%= render "shared/icons/check", class: "flex-shrink-0 me-2" %>
|
62
|
+
<%= render "good_job/shared/icons/check", class: "flex-shrink-0 me-2" %>
|
63
63
|
<div><%= alert %></div>
|
64
64
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
65
65
|
</div>
|
data/engine/config/routes.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
GoodJob::Engine.routes.draw do
|
2
3
|
root to: 'dashboards#index'
|
3
4
|
resources :active_jobs, only: %i[show]
|
4
5
|
resources :jobs, only: %i[destroy]
|
5
6
|
|
6
7
|
scope controller: :assets do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
constraints(format: :css) do
|
9
|
+
get :bootstrap, action: :bootstrap_css
|
10
|
+
get :chartist, action: :chartist_css
|
11
|
+
get :style, action: :style_css
|
12
|
+
end
|
13
|
+
|
14
|
+
constraints(format: :js) do
|
15
|
+
get :bootstrap, action: :bootstrap_js
|
16
|
+
get :chartist, action: :chartist_js
|
17
|
+
end
|
12
18
|
end
|
13
19
|
end
|
data/exe/good_job
CHANGED
data/lib/good_job.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "rails"
|
2
3
|
require "active_job"
|
3
4
|
require "active_job/queue_adapters"
|
@@ -105,16 +106,13 @@ module GoodJob
|
|
105
106
|
wait ? -1 : nil
|
106
107
|
end
|
107
108
|
|
108
|
-
|
109
|
-
_shutdown_all(executables, timeout: timeout)
|
109
|
+
_shutdown_all(_executables, timeout: timeout)
|
110
110
|
end
|
111
111
|
|
112
112
|
# Tests whether jobs have stopped executing.
|
113
113
|
# @return [Boolean] whether background threads are shut down
|
114
114
|
def self.shutdown?
|
115
|
-
|
116
|
-
Poller.instances.all?(&:shutdown?) &&
|
117
|
-
Scheduler.instances.all?(&:shutdown?)
|
115
|
+
_executables.all?(&:shutdown?)
|
118
116
|
end
|
119
117
|
|
120
118
|
# Stops and restarts executing jobs.
|
@@ -125,8 +123,7 @@ module GoodJob
|
|
125
123
|
# @param timeout [Numeric, nil] Seconds to wait for active threads to finish.
|
126
124
|
# @return [void]
|
127
125
|
def self.restart(timeout: -1)
|
128
|
-
|
129
|
-
_shutdown_all(executables, :restart, timeout: timeout)
|
126
|
+
_shutdown_all(_executables, :restart, timeout: timeout)
|
130
127
|
end
|
131
128
|
|
132
129
|
# Sends +#shutdown+ or +#restart+ to executable objects ({GoodJob::Notifier}, {GoodJob::Poller}, {GoodJob::Scheduler})
|
@@ -145,5 +142,14 @@ module GoodJob
|
|
145
142
|
end
|
146
143
|
end
|
147
144
|
|
145
|
+
def self._executables
|
146
|
+
[].concat(
|
147
|
+
CronManager.instances,
|
148
|
+
Notifier.instances,
|
149
|
+
Poller.instances,
|
150
|
+
Scheduler.instances
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
148
154
|
ActiveSupport.run_load_hooks(:good_job, self)
|
149
155
|
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GoodJob
|
2
3
|
#
|
3
4
|
# ActiveJob Adapter.
|
@@ -56,6 +57,8 @@ module GoodJob
|
|
56
57
|
@scheduler = GoodJob::Scheduler.from_configuration(@configuration, warm_cache_on_initialize: Rails.application.initialized?)
|
57
58
|
@notifier.recipients << [@scheduler, :create_thread]
|
58
59
|
@poller.recipients << [@scheduler, :create_thread]
|
60
|
+
|
61
|
+
@cron_manager = GoodJob::CronManager.new(@configuration.cron, start_on_initialize: Rails.application.initialized?) if @configuration.enable_cron?
|
59
62
|
end
|
60
63
|
end
|
61
64
|
|
data/lib/good_job/cli.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'thor'
|
2
3
|
|
3
4
|
module GoodJob
|
@@ -69,12 +70,16 @@ module GoodJob
|
|
69
70
|
type: :numeric,
|
70
71
|
banner: 'SECONDS',
|
71
72
|
desc: "Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))"
|
73
|
+
method_option :enable_cron,
|
74
|
+
type: :boolean,
|
75
|
+
desc: "Whether to run cron process (default: false)"
|
72
76
|
method_option :daemonize,
|
73
77
|
type: :boolean,
|
74
78
|
desc: "Run as a background daemon (default: false)"
|
75
79
|
method_option :pidfile,
|
76
80
|
type: :string,
|
77
81
|
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
|
82
|
+
|
78
83
|
def start
|
79
84
|
set_up_application!
|
80
85
|
configuration = GoodJob::Configuration.new(options)
|
@@ -86,7 +91,7 @@ module GoodJob
|
|
86
91
|
scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: true)
|
87
92
|
notifier.recipients << [scheduler, :create_thread]
|
88
93
|
poller.recipients << [scheduler, :create_thread]
|
89
|
-
|
94
|
+
cron_manager = GoodJob::CronManager.new(configuration.cron, start_on_initialize: true) if configuration.enable_cron?
|
90
95
|
@stop_good_job_executable = false
|
91
96
|
%w[INT TERM].each do |signal|
|
92
97
|
trap(signal) { @stop_good_job_executable = true }
|
@@ -97,7 +102,7 @@ module GoodJob
|
|
97
102
|
break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
|
98
103
|
end
|
99
104
|
|
100
|
-
executors = [notifier, poller, scheduler]
|
105
|
+
executors = [notifier, poller, cron_manager, scheduler].compact
|
101
106
|
GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
|
102
107
|
end
|
103
108
|
|
@@ -123,6 +128,7 @@ module GoodJob
|
|
123
128
|
type: :numeric,
|
124
129
|
banner: 'SECONDS',
|
125
130
|
desc: "Delete records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)"
|
131
|
+
|
126
132
|
def cleanup_preserved_jobs
|
127
133
|
set_up_application!
|
128
134
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GoodJob
|
2
3
|
#
|
3
4
|
# +GoodJob::Configuration+ provides normalized configuration information to
|
@@ -17,6 +18,8 @@ module GoodJob
|
|
17
18
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
|
18
19
|
# Default to always wait for jobs to finish for {Adapter#shutdown}
|
19
20
|
DEFAULT_SHUTDOWN_TIMEOUT = -1
|
21
|
+
# Default to not running cron
|
22
|
+
DEFAULT_ENABLE_CRON = false
|
20
23
|
|
21
24
|
# The options that were explicitly set when initializing +Configuration+.
|
22
25
|
# @return [Hash]
|
@@ -128,6 +131,28 @@ module GoodJob
|
|
128
131
|
).to_f
|
129
132
|
end
|
130
133
|
|
134
|
+
# Whether to run cron
|
135
|
+
# @return [Boolean]
|
136
|
+
def enable_cron
|
137
|
+
value = ActiveModel::Type::Boolean.new.cast(
|
138
|
+
options[:enable_cron] ||
|
139
|
+
rails_config[:enable_cron] ||
|
140
|
+
env['GOOD_JOB_ENABLE_CRON'] ||
|
141
|
+
false
|
142
|
+
)
|
143
|
+
value && cron.size.positive?
|
144
|
+
end
|
145
|
+
alias enable_cron? enable_cron
|
146
|
+
|
147
|
+
def cron
|
148
|
+
env_cron = JSON.parse(ENV['GOOD_JOB_CRON']) if ENV['GOOD_JOB_CRON'].present?
|
149
|
+
|
150
|
+
options[:cron] ||
|
151
|
+
rails_config[:cron] ||
|
152
|
+
env_cron ||
|
153
|
+
{}
|
154
|
+
end
|
155
|
+
|
131
156
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
132
157
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
133
158
|
# @return [Integer]
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "concurrent/hash"
|
3
|
+
require "concurrent/scheduled_task"
|
4
|
+
require "fugit"
|
5
|
+
|
6
|
+
module GoodJob # :nodoc:
|
7
|
+
#
|
8
|
+
# CronManagers enqueue jobs on a repeating schedule.
|
9
|
+
#
|
10
|
+
class CronManager
|
11
|
+
# @!attribute [r] instances
|
12
|
+
# @!scope class
|
13
|
+
# List of all instantiated CronManagers in the current process.
|
14
|
+
# @return [Array<GoodJob::CronManagers>, nil]
|
15
|
+
cattr_reader :instances, default: [], instance_reader: false
|
16
|
+
|
17
|
+
# Task observer for cron task
|
18
|
+
# @param time [Time]
|
19
|
+
# @param output [Object]
|
20
|
+
# @param thread_error [Exception]
|
21
|
+
def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
|
22
|
+
return if thread_error.is_a? Concurrent::CancelledOperationError
|
23
|
+
|
24
|
+
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Job configuration to be scheduled
|
28
|
+
# @return [Hash]
|
29
|
+
attr_reader :schedules
|
30
|
+
|
31
|
+
# @param schedules [Hash]
|
32
|
+
# @param start_on_initialize [Boolean]
|
33
|
+
def initialize(schedules = {}, start_on_initialize: false)
|
34
|
+
@running = false
|
35
|
+
@schedules = schedules
|
36
|
+
@tasks = Concurrent::Hash.new
|
37
|
+
|
38
|
+
self.class.instances << self
|
39
|
+
|
40
|
+
start if start_on_initialize
|
41
|
+
end
|
42
|
+
|
43
|
+
# Schedule tasks that will enqueue jobs based on their schedule
|
44
|
+
def start
|
45
|
+
ActiveSupport::Notifications.instrument("cron_manager_start.good_job", cron_jobs: @schedules) do
|
46
|
+
@running = true
|
47
|
+
schedules.each_key { |cron_key| create_task(cron_key) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Stop/cancel any scheduled tasks
|
52
|
+
# @param timeout [Numeric, nil] Unused but retained for compatibility
|
53
|
+
def shutdown(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
|
54
|
+
@running = false
|
55
|
+
@tasks.each do |_cron_key, task|
|
56
|
+
task.cancel
|
57
|
+
end
|
58
|
+
@tasks.clear
|
59
|
+
end
|
60
|
+
|
61
|
+
# Stop and restart
|
62
|
+
# @param timeout [Numeric, nil] Unused but retained for compatibility
|
63
|
+
def restart(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
|
64
|
+
shutdown
|
65
|
+
start
|
66
|
+
end
|
67
|
+
|
68
|
+
# Tests whether the manager is running.
|
69
|
+
# @return [Boolean, nil]
|
70
|
+
def running?
|
71
|
+
@running
|
72
|
+
end
|
73
|
+
|
74
|
+
# Tests whether the manager is shutdown.
|
75
|
+
# @return [Boolean, nil]
|
76
|
+
def shutdown?
|
77
|
+
!running?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Enqueues a scheduled task
|
81
|
+
# @param cron_key [Symbol, String] the key within the schedule to use
|
82
|
+
def create_task(cron_key)
|
83
|
+
schedule = @schedules[cron_key]
|
84
|
+
return false if schedule.blank?
|
85
|
+
|
86
|
+
fugit = Fugit::Cron.parse(schedule.fetch(:cron))
|
87
|
+
delay = [(fugit.next_time - Time.current).to_f, 0].max
|
88
|
+
|
89
|
+
future = Concurrent::ScheduledTask.new(delay, args: [self, cron_key]) do |thr_scheduler, thr_cron_key|
|
90
|
+
# Re-schedule the next cron task before executing the current task
|
91
|
+
thr_scheduler.create_task(thr_cron_key)
|
92
|
+
|
93
|
+
CurrentExecution.reset
|
94
|
+
CurrentExecution.cron_key = thr_cron_key
|
95
|
+
|
96
|
+
Rails.application.executor.wrap do
|
97
|
+
schedule = thr_scheduler.schedules.fetch(thr_cron_key).with_indifferent_access
|
98
|
+
job_class = schedule.fetch(:class).constantize
|
99
|
+
|
100
|
+
job_set_value = schedule.fetch(:set, {})
|
101
|
+
job_set = job_set_value.respond_to?(:call) ? job_set_value.call : job_set_value
|
102
|
+
|
103
|
+
job_args_value = schedule.fetch(:args, [])
|
104
|
+
job_args = job_args_value.respond_to?(:call) ? job_args_value.call : job_args_value
|
105
|
+
|
106
|
+
job_class.set(job_set).perform_later(*job_args)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
@tasks[cron_key] = future
|
111
|
+
future.add_observer(self.class, :task_observer)
|
112
|
+
future.execute
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
2
3
|
|
3
4
|
module GoodJob
|
@@ -10,6 +11,12 @@ module GoodJob
|
|
10
11
|
# @return [String, nil]
|
11
12
|
thread_mattr_accessor :active_job_id
|
12
13
|
|
14
|
+
# @!attribute [rw] cron_key
|
15
|
+
# @!scope class
|
16
|
+
# Cron Key
|
17
|
+
# @return [String, nil]
|
18
|
+
thread_mattr_accessor :cron_key
|
19
|
+
|
13
20
|
# @!attribute [rw] error_on_discard
|
14
21
|
# @!scope class
|
15
22
|
# Error captured by discard_on
|
data/lib/good_job/daemon.rb
CHANGED
data/lib/good_job/job.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GoodJob
|
2
3
|
# ActiveRecord model that represents an +ActiveJob+ job.
|
3
4
|
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
@@ -10,11 +11,12 @@ module GoodJob
|
|
10
11
|
PreviouslyPerformedError = Class.new(StandardError)
|
11
12
|
|
12
13
|
# ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
|
13
|
-
DEFAULT_QUEUE_NAME = 'default'
|
14
|
+
DEFAULT_QUEUE_NAME = 'default'
|
14
15
|
# ActiveJob jobs without a +priority+ attribute are given this priority.
|
15
16
|
DEFAULT_PRIORITY = 0
|
16
17
|
|
17
|
-
self.table_name = 'good_jobs'
|
18
|
+
self.table_name = 'good_jobs'
|
19
|
+
self.advisory_lockable_column = 'id'
|
18
20
|
|
19
21
|
attr_readonly :serialized_params
|
20
22
|
|
@@ -197,6 +199,7 @@ module GoodJob
|
|
197
199
|
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
198
200
|
ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
|
199
201
|
good_job_args = {
|
202
|
+
cron_key: CurrentExecution.cron_key,
|
200
203
|
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
201
204
|
priority: active_job.priority || DEFAULT_PRIORITY,
|
202
205
|
serialized_params: active_job.serialize,
|
@@ -283,6 +286,25 @@ module GoodJob
|
|
283
286
|
super || serialized_params['job_id']
|
284
287
|
end
|
285
288
|
|
289
|
+
def cron_key
|
290
|
+
if self.class.column_names.include?('cron_key')
|
291
|
+
super
|
292
|
+
else
|
293
|
+
ActiveSupport::Deprecation.warn(<<~DEPRECATION)
|
294
|
+
GoodJob has pending database migrations. To create the migration files, run:
|
295
|
+
|
296
|
+
rails generate good_job:update
|
297
|
+
|
298
|
+
To apply the migration files, run:
|
299
|
+
|
300
|
+
rails db:migrate
|
301
|
+
|
302
|
+
DEPRECATION
|
303
|
+
|
304
|
+
nil
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
286
308
|
private
|
287
309
|
|
288
310
|
# @return [ExecutionResult]
|
@@ -293,6 +315,7 @@ module GoodJob
|
|
293
315
|
|
294
316
|
GoodJob::CurrentExecution.reset
|
295
317
|
GoodJob::CurrentExecution.active_job_id = active_job_id
|
318
|
+
GoodJob::CurrentExecution.cron_key = cron_key
|
296
319
|
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
297
320
|
value = ActiveJob::Base.execute(params)
|
298
321
|
|
data/lib/good_job/lockable.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GoodJob
|
2
3
|
#
|
3
4
|
# Adds Postgres advisory locking capabilities to an ActiveRecord record.
|
@@ -23,20 +24,20 @@ module GoodJob
|
|
23
24
|
|
24
25
|
included do
|
25
26
|
# Default column to be used when creating Advisory Locks
|
26
|
-
|
27
|
+
class_attribute :advisory_lockable_column, instance_accessor: false, default: Concurrent::Delay.new { primary_key }
|
27
28
|
|
28
29
|
# Default Postgres function to be used for Advisory Locks
|
29
|
-
|
30
|
+
class_attribute :advisory_lockable_function, default: "pg_try_advisory_lock"
|
30
31
|
|
31
32
|
# Attempt to acquire an advisory lock on the selected records and
|
32
33
|
# return only those records for which a lock could be acquired.
|
33
|
-
# @!method advisory_lock(column:
|
34
|
+
# @!method advisory_lock(column: _advisory_lockable_column, function: advisory_lockable_function)
|
34
35
|
# @!scope class
|
35
36
|
# @param column [String, Symbol] column values to Advisory Lock against
|
36
37
|
# @param function [String, Symbol] Postgres Advisory Lock function name to use
|
37
38
|
# @return [ActiveRecord::Relation]
|
38
39
|
# A relation selecting only the records that were locked.
|
39
|
-
scope :advisory_lock, (lambda do |column:
|
40
|
+
scope :advisory_lock, (lambda do |column: _advisory_lockable_column, function: advisory_lockable_function|
|
40
41
|
original_query = self
|
41
42
|
|
42
43
|
cte_table = Arel::Table.new(:rows)
|
@@ -64,13 +65,13 @@ module GoodJob
|
|
64
65
|
#
|
65
66
|
# For details on +pg_locks+, see
|
66
67
|
# {https://www.postgresql.org/docs/current/view-pg-locks.html}.
|
67
|
-
# @!method joins_advisory_locks(column:
|
68
|
+
# @!method joins_advisory_locks(column: _advisory_lockable_column)
|
68
69
|
# @!scope class
|
69
70
|
# @param column [String, Symbol] column values to Advisory Lock against
|
70
71
|
# @return [ActiveRecord::Relation]
|
71
72
|
# @example Get the records that have a session awaiting a lock:
|
72
73
|
# MyLockableRecord.joins_advisory_locks.where("pg_locks.granted = ?", false)
|
73
|
-
scope :joins_advisory_locks, (lambda do |column:
|
74
|
+
scope :joins_advisory_locks, (lambda do |column: _advisory_lockable_column|
|
74
75
|
join_sql = <<~SQL.squish
|
75
76
|
LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
|
76
77
|
AND pg_locks.objsubid = 1
|
@@ -82,26 +83,26 @@ module GoodJob
|
|
82
83
|
end)
|
83
84
|
|
84
85
|
# Find records that do not have an advisory lock on them.
|
85
|
-
# @!method advisory_unlocked(column:
|
86
|
+
# @!method advisory_unlocked(column: _advisory_lockable_column)
|
86
87
|
# @!scope class
|
87
88
|
# @param column [String, Symbol] column values to Advisory Lock against
|
88
89
|
# @return [ActiveRecord::Relation]
|
89
|
-
scope :advisory_unlocked, ->(column:
|
90
|
+
scope :advisory_unlocked, ->(column: _advisory_lockable_column) { joins_advisory_locks(column: column).where(pg_locks: { locktype: nil }) }
|
90
91
|
|
91
92
|
# Find records that have an advisory lock on them.
|
92
|
-
# @!method advisory_locked(column:
|
93
|
+
# @!method advisory_locked(column: _advisory_lockable_column)
|
93
94
|
# @!scope class
|
94
95
|
# @param column [String, Symbol] column values to Advisory Lock against
|
95
96
|
# @return [ActiveRecord::Relation]
|
96
|
-
scope :advisory_locked, ->(column:
|
97
|
+
scope :advisory_locked, ->(column: _advisory_lockable_column) { joins_advisory_locks(column: column).where.not(pg_locks: { locktype: nil }) }
|
97
98
|
|
98
99
|
# Find records with advisory locks owned by the current Postgres
|
99
100
|
# session/connection.
|
100
|
-
# @!method advisory_locked(column:
|
101
|
+
# @!method advisory_locked(column: _advisory_lockable_column)
|
101
102
|
# @!scope class
|
102
103
|
# @param column [String, Symbol] column values to Advisory Lock against
|
103
104
|
# @return [ActiveRecord::Relation]
|
104
|
-
scope :owns_advisory_locked, ->(column:
|
105
|
+
scope :owns_advisory_locked, ->(column: _advisory_lockable_column) { joins_advisory_locks(column: column).where('"pg_locks"."pid" = pg_backend_pid()') }
|
105
106
|
|
106
107
|
# Whether an advisory lock should be acquired in the same transaction
|
107
108
|
# that created the record.
|
@@ -143,7 +144,7 @@ module GoodJob
|
|
143
144
|
# MyLockableRecord.order(created_at: :asc).limit(2).with_advisory_lock do |record|
|
144
145
|
# do_something_with record
|
145
146
|
# end
|
146
|
-
def with_advisory_lock(column:
|
147
|
+
def with_advisory_lock(column: _advisory_lockable_column, function: advisory_lockable_function, unlock_session: false)
|
147
148
|
raise ArgumentError, "Must provide a block" unless block_given?
|
148
149
|
|
149
150
|
records = advisory_lock(column: column, function: function).to_a
|
@@ -154,13 +155,19 @@ module GoodJob
|
|
154
155
|
advisory_unlock_session
|
155
156
|
else
|
156
157
|
records.each do |record|
|
157
|
-
key = [table_name, record[
|
158
|
+
key = [table_name, record[_advisory_lockable_column]].join
|
158
159
|
record.advisory_unlock(key: key, function: advisory_unlockable_function(function))
|
159
160
|
end
|
160
161
|
end
|
161
162
|
end
|
162
163
|
end
|
163
164
|
|
165
|
+
# Allow advisory_lockable_column to be a `Concurrent::Delay`
|
166
|
+
def _advisory_lockable_column
|
167
|
+
column = advisory_lockable_column
|
168
|
+
column.respond_to?(:value) ? column.value : column
|
169
|
+
end
|
170
|
+
|
164
171
|
def supports_cte_materialization_specifiers?
|
165
172
|
return @_supports_cte_materialization_specifiers if defined?(@_supports_cte_materialization_specifiers)
|
166
173
|
|
@@ -308,7 +315,7 @@ module GoodJob
|
|
308
315
|
# Default Advisory Lock key
|
309
316
|
# @return [String]
|
310
317
|
def lockable_key
|
311
|
-
[self.class.table_name, self[self.class.
|
318
|
+
[self.class.table_name, self[self.class._advisory_lockable_column]].join
|
312
319
|
end
|
313
320
|
|
314
321
|
delegate :pg_or_jdbc_query, to: :class
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GoodJob
|
2
3
|
#
|
3
4
|
# Listens to GoodJob notifications and logs them.
|
@@ -56,6 +57,16 @@ module GoodJob
|
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
60
|
+
# @!macro notification_responder
|
61
|
+
def cron_manager_start(event)
|
62
|
+
cron_jobs = event.payload[:cron_jobs]
|
63
|
+
cron_jobs_count = cron_jobs.size
|
64
|
+
|
65
|
+
info do
|
66
|
+
"GoodJob started cron with #{cron_jobs_count} #{'jobs'.pluralize(cron_jobs_count)}."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
59
70
|
# @!macro notification_responder
|
60
71
|
def scheduler_shutdown_start(event)
|
61
72
|
process_id = event.payload[:process_id]
|
data/lib/good_job/notifier.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'concurrent/atomic/atomic_boolean'
|
2
3
|
|
3
4
|
module GoodJob # :nodoc:
|
@@ -13,7 +14,7 @@ module GoodJob # :nodoc:
|
|
13
14
|
AdapterCannotListenError = Class.new(StandardError)
|
14
15
|
|
15
16
|
# Default Postgres channel for LISTEN/NOTIFY
|
16
|
-
CHANNEL = 'good_job'
|
17
|
+
CHANNEL = 'good_job'
|
17
18
|
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
18
19
|
EXECUTOR_OPTIONS = {
|
19
20
|
name: name,
|
@@ -24,6 +25,8 @@ module GoodJob # :nodoc:
|
|
24
25
|
max_queue: 1,
|
25
26
|
fallback_policy: :discard,
|
26
27
|
}.freeze
|
28
|
+
# Seconds to wait if database cannot be connected to
|
29
|
+
RECONNECT_INTERVAL = 5
|
27
30
|
# Seconds to block while LISTENing for a message
|
28
31
|
WAIT_INTERVAL = 1
|
29
32
|
|
@@ -114,7 +117,13 @@ module GoodJob # :nodoc:
|
|
114
117
|
ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: thread_error })
|
115
118
|
end
|
116
119
|
|
117
|
-
|
120
|
+
return if shutdown?
|
121
|
+
|
122
|
+
if thread_error.is_a?(ActiveRecord::ConnectionNotEstablished) || thread_error.is_a?(ActiveRecord::StatementInvalid)
|
123
|
+
listen(delay: RECONNECT_INTERVAL)
|
124
|
+
else
|
125
|
+
listen
|
126
|
+
end
|
118
127
|
end
|
119
128
|
|
120
129
|
private
|
@@ -125,8 +134,8 @@ module GoodJob # :nodoc:
|
|
125
134
|
@executor = Concurrent::ThreadPoolExecutor.new(EXECUTOR_OPTIONS)
|
126
135
|
end
|
127
136
|
|
128
|
-
def listen
|
129
|
-
future = Concurrent::
|
137
|
+
def listen(delay: 0)
|
138
|
+
future = Concurrent::ScheduledTask.new(delay, args: [@recipients, executor, @listening], executor: @executor) do |thr_recipients, thr_executor, thr_listening|
|
130
139
|
with_listen_connection do |conn|
|
131
140
|
ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
|
132
141
|
conn.async_exec("LISTEN #{CHANNEL}").clear
|
data/lib/good_job/poller.rb
CHANGED
data/lib/good_job/railtie.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GoodJob
|
2
3
|
# Ruby on Rails integration.
|
3
4
|
class Railtie < ::Rails::Railtie
|
4
5
|
config.good_job = ActiveSupport::OrderedOptions.new
|
6
|
+
config.good_job.cron = {}
|
5
7
|
|
6
8
|
initializer "good_job.logger" do |_app|
|
7
9
|
ActiveSupport.on_load(:good_job) do
|
@@ -22,6 +24,7 @@ module GoodJob
|
|
22
24
|
|
23
25
|
config.after_initialize do
|
24
26
|
GoodJob::Scheduler.instances.each(&:warm_cache)
|
27
|
+
GoodJob::CronManager.instances.each(&:start)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
end
|
data/lib/good_job/scheduler.rb
CHANGED
data/lib/good_job/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.12.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: 2021-07-
|
11
|
+
date: 2021-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 1.0.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fugit
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: railties
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -345,12 +359,12 @@ files:
|
|
345
359
|
- engine/app/helpers/good_job/application_helper.rb
|
346
360
|
- engine/app/views/good_job/active_jobs/show.html.erb
|
347
361
|
- engine/app/views/good_job/dashboards/index.html.erb
|
362
|
+
- engine/app/views/good_job/shared/_chart.erb
|
363
|
+
- engine/app/views/good_job/shared/_jobs_table.erb
|
364
|
+
- engine/app/views/good_job/shared/icons/_check.html.erb
|
365
|
+
- engine/app/views/good_job/shared/icons/_exclamation.html.erb
|
366
|
+
- engine/app/views/good_job/shared/icons/_trash.html.erb
|
348
367
|
- engine/app/views/layouts/good_job/base.html.erb
|
349
|
-
- engine/app/views/shared/_chart.erb
|
350
|
-
- engine/app/views/shared/_jobs_table.erb
|
351
|
-
- engine/app/views/shared/icons/_check.html.erb
|
352
|
-
- engine/app/views/shared/icons/_exclamation.html.erb
|
353
|
-
- engine/app/views/shared/icons/_trash.html.erb
|
354
368
|
- engine/config/routes.rb
|
355
369
|
- engine/lib/good_job/engine.rb
|
356
370
|
- exe/good_job
|
@@ -367,6 +381,7 @@ files:
|
|
367
381
|
- lib/good_job/adapter.rb
|
368
382
|
- lib/good_job/cli.rb
|
369
383
|
- lib/good_job/configuration.rb
|
384
|
+
- lib/good_job/cron_manager.rb
|
370
385
|
- lib/good_job/current_execution.rb
|
371
386
|
- lib/good_job/daemon.rb
|
372
387
|
- lib/good_job/execution_result.rb
|