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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -2
  3. data/README.md +47 -3
  4. data/engine/app/controllers/good_job/active_jobs_controller.rb +1 -0
  5. data/engine/app/controllers/good_job/assets_controller.rb +1 -0
  6. data/engine/app/controllers/good_job/base_controller.rb +1 -0
  7. data/engine/app/controllers/good_job/dashboards_controller.rb +1 -0
  8. data/engine/app/controllers/good_job/jobs_controller.rb +1 -0
  9. data/engine/app/helpers/good_job/application_helper.rb +1 -0
  10. data/engine/app/views/good_job/active_jobs/show.html.erb +1 -1
  11. data/engine/app/views/good_job/dashboards/index.html.erb +2 -2
  12. data/engine/app/views/{shared → good_job/shared}/_chart.erb +2 -2
  13. data/engine/app/views/{shared → good_job/shared}/_jobs_table.erb +1 -1
  14. data/engine/app/views/{shared → good_job/shared}/icons/_check.html.erb +0 -0
  15. data/engine/app/views/{shared → good_job/shared}/icons/_exclamation.html.erb +0 -0
  16. data/engine/app/views/{shared → good_job/shared}/icons/_trash.html.erb +0 -0
  17. data/engine/app/views/layouts/good_job/base.html.erb +7 -7
  18. data/engine/config/routes.rb +11 -5
  19. data/engine/lib/good_job/engine.rb +1 -0
  20. data/exe/good_job +1 -0
  21. data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
  22. data/lib/generators/good_job/install_generator.rb +1 -0
  23. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb +1 -0
  24. data/lib/generators/good_job/templates/update/migrations/02_add_active_job_id_concurrency_key_cron_key_to_good_jobs.rb +1 -0
  25. data/lib/generators/good_job/templates/update/migrations/03_add_active_job_id_index_and_concurrency_key_index_to_good_jobs.rb +1 -0
  26. data/lib/generators/good_job/update_generator.rb +1 -0
  27. data/lib/good_job.rb +13 -7
  28. data/lib/good_job/active_job_extensions.rb +1 -0
  29. data/lib/good_job/active_job_extensions/concurrency.rb +1 -0
  30. data/lib/good_job/adapter.rb +3 -0
  31. data/lib/good_job/cli.rb +8 -2
  32. data/lib/good_job/configuration.rb +25 -0
  33. data/lib/good_job/cron_manager.rb +115 -0
  34. data/lib/good_job/current_execution.rb +7 -0
  35. data/lib/good_job/daemon.rb +1 -0
  36. data/lib/good_job/execution_result.rb +1 -0
  37. data/lib/good_job/job.rb +25 -2
  38. data/lib/good_job/job_performer.rb +1 -0
  39. data/lib/good_job/lockable.rb +22 -15
  40. data/lib/good_job/log_subscriber.rb +11 -0
  41. data/lib/good_job/multi_scheduler.rb +1 -0
  42. data/lib/good_job/notifier.rb +13 -4
  43. data/lib/good_job/poller.rb +1 -0
  44. data/lib/good_job/railtie.rb +3 -0
  45. data/lib/good_job/scheduler.rb +1 -0
  46. data/lib/good_job/version.rb +2 -1
  47. metadata +22 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c0b45fcd80fbd536af0f6fed44ef53f446ee6574f7e90d41a38585ec8b3ba63
4
- data.tar.gz: 8b45f77d94bd809b25fb725f57622f1966f188157f235e03c56442817e31afd4
3
+ metadata.gz: bdf4b1eebb1224acd53f07753d5cda9cb9fde7a44baf300824d7816da94980f2
4
+ data.tar.gz: 4b90c9cd8133f179f87094683218ed1a1aed3582512169dd914a00dd62a81628
5
5
  SHA512:
6
- metadata.gz: 820a1847c2dfce6456d925fe73e8373d5b2f19ff9bc2dd143dbf942acfb0c3872b4fdd1dc888741ae7f3974c7f6869ef82ef8c54bd238ceea03bb63ff8829593
7
- data.tar.gz: 4faa2442654f37475c22ce7c3e3b440351a5f91a53ac2ada273cba764c409e7f7f108c26b463a736b9880d4097ef9096775905c092bf5860805e3cf7bffede86
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
- **Merged pull requests:**
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 Concurrency](#activejob-concurrency)
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 Concurrency
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,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  class ActiveJobsController < GoodJob::BaseController
3
4
  def show
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  class AssetsController < ActionController::Base # rubocop:disable Rails/ApplicationController
3
4
  skip_before_action :verify_authenticity_token, raise: false
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  class BaseController < ActionController::Base # rubocop:disable Rails/ApplicationController
3
4
  protect_from_forgery with: :exception
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  class DashboardsController < GoodJob::BaseController
3
4
  class JobFilter
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  class JobsController < GoodJob::BaseController
3
4
  def destroy
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  module ApplicationHelper
3
4
  end
@@ -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
- <script>
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
- </script>
52
+ <% end %>
@@ -29,7 +29,7 @@
29
29
  </td>
30
30
  <td>
31
31
  <%= button_to job_path(job.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete job" do %>
32
- <%= render "shared/icons/trash" %>
32
+ <%= render "good_job/shared/icons/trash" %>
33
33
  <% end %>
34
34
  </td>
35
35
  </tr>
@@ -5,12 +5,12 @@
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
 
8
- <%= stylesheet_link_tag bootstrap_css_path(v: GoodJob::VERSION) %>
9
- <%= stylesheet_link_tag chartist_css_path(v: GoodJob::VERSION) %>
10
- <%= stylesheet_link_tag style_css_path(v: GoodJob::VERSION) %>
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 bootstrap_js_path(v: GoodJob::VERSION) %>
13
- <%= javascript_include_tag chartist_js_path(v: GoodJob::VERSION) %>
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>
@@ -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
- get :bootstrap_css
8
- get :bootstrap_js
9
- get :chartist_css
10
- get :chartist_js
11
- get :style_css
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  class Engine < ::Rails::Engine
3
4
  isolate_namespace GoodJob
data/exe/good_job CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
  require 'good_job/cli'
3
4
 
4
5
  GoodJob::CLI.within_exe = true
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveJob # :nodoc:
2
3
  module QueueAdapters # :nodoc:
3
4
  # See {GoodJob::Adapter} for details.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators'
2
3
  require 'rails/generators/active_record'
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class CreateGoodJobs < ActiveRecord::Migration[5.2]
2
3
  def change
3
4
  enable_extension 'pgcrypto'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class AddActiveJobIdConcurrencyKeyCronKeyToGoodJobs < ActiveRecord::Migration[5.2]
2
3
  def change
3
4
  reversible do |dir|
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class AddActiveJobIdIndexAndConcurrencyKeyIndexToGoodJobs < ActiveRecord::Migration[5.2]
2
3
  disable_ddl_transaction!
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators'
2
3
  require 'rails/generators/active_record'
3
4
 
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
- executables = Array(Notifier.instances) + Array(Poller.instances) + Array(Scheduler.instances)
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
- Notifier.instances.all?(&:shutdown?) &&
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
- executables = Array(Notifier.instances) + Array(Poller.instances) + Array(Scheduler.instances)
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  module ActiveJobExtensions
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  module ActiveJobExtensions
3
4
  module Concurrency
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  #
3
4
  # Manages daemonization of the current process.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  # Stores the results of job execution
3
4
  class ExecutionResult
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'.freeze
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'.freeze
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
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'concurrent/delay'
2
3
 
3
4
  module GoodJob
@@ -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
- cattr_accessor(:advisory_lockable_column, instance_accessor: false) { primary_key }
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
- cattr_accessor(:advisory_lockable_function) { "pg_try_advisory_lock" }
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: advisory_lockable_column, function: advisory_lockable_function)
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: advisory_lockable_column, function: advisory_lockable_function|
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: advisory_lockable_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: advisory_lockable_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: advisory_lockable_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: advisory_lockable_column) { joins_advisory_locks(column: column).where(pg_locks: { locktype: nil }) }
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: advisory_lockable_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: advisory_lockable_column) { joins_advisory_locks(column: column).where.not(pg_locks: { locktype: nil }) }
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: advisory_lockable_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: advisory_lockable_column) { joins_advisory_locks(column: column).where('"pg_locks"."pid" = pg_backend_pid()') }
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: advisory_lockable_column, function: advisory_lockable_function, unlock_session: false)
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[advisory_lockable_column]].join
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.advisory_lockable_column]].join
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]
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  # Delegates the interface of a single {Scheduler} to multiple Schedulers.
3
4
  class MultiScheduler
@@ -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'.freeze
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
- listen unless shutdown?
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::Future.new(args: [@recipients, executor, @listening], executor: @executor) do |thr_recipients, thr_executor, thr_listening|
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'concurrent/atomic/atomic_boolean'
2
3
 
3
4
  module GoodJob # :nodoc:
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "concurrent/executor/thread_pool_executor"
2
3
  require "concurrent/executor/timer_set"
3
4
  require "concurrent/scheduled_task"
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  module GoodJob
2
3
  # GoodJob gem version.
3
- VERSION = '1.11.0'.freeze
4
+ VERSION = '1.12.0'
4
5
  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.11.0
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-07 00:00:00.000000000 Z
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