good_job 4.22.1 → 4.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/README.md +20 -2
  4. data/app/charts/good_job/base_chart.rb +1 -0
  5. data/app/charts/good_job/latency_metric/base.rb +2 -1
  6. data/app/charts/good_job/latency_metric/execution.rb +1 -0
  7. data/app/charts/good_job/latency_metric/queue.rb +1 -0
  8. data/app/charts/good_job/latency_metric/total.rb +1 -0
  9. data/app/charts/good_job/latency_metric.rb +1 -0
  10. data/app/charts/good_job/performance_index_chart.rb +2 -1
  11. data/app/charts/good_job/performance_range.rb +5 -3
  12. data/app/charts/good_job/performance_show_chart.rb +1 -0
  13. data/app/charts/good_job/scheduled_by_queue_chart.rb +2 -1
  14. data/app/controllers/good_job/application_controller.rb +7 -4
  15. data/app/controllers/good_job/batches_controller.rb +1 -0
  16. data/app/controllers/good_job/cleaner_controller.rb +1 -0
  17. data/app/controllers/good_job/cron_entries_controller.rb +1 -0
  18. data/app/controllers/good_job/executions_controller.rb +12 -0
  19. data/app/controllers/good_job/frontends_controller.rb +9 -4
  20. data/app/controllers/good_job/jobs_controller.rb +1 -0
  21. data/app/controllers/good_job/metrics_controller.rb +4 -1
  22. data/app/controllers/good_job/pauses_controller.rb +1 -0
  23. data/app/controllers/good_job/performance_controller.rb +1 -0
  24. data/app/controllers/good_job/processes_controller.rb +1 -0
  25. data/app/filters/good_job/base_filter.rb +10 -1
  26. data/app/filters/good_job/batches_filter.rb +1 -0
  27. data/app/filters/good_job/jobs_filter.rb +4 -1
  28. data/app/helpers/good_job/application_helper.rb +1 -0
  29. data/app/helpers/good_job/icons_helper.rb +1 -0
  30. data/app/models/concerns/good_job/advisory_lockable.rb +23 -5
  31. data/app/models/concerns/good_job/error_events.rb +1 -0
  32. data/app/models/concerns/good_job/filterable.rb +1 -0
  33. data/app/models/concerns/good_job/filtered_arguments.rb +28 -0
  34. data/app/models/concerns/good_job/reportable.rb +1 -0
  35. data/app/models/good_job/active_record_parent_class.rb +1 -0
  36. data/app/models/good_job/base_record.rb +1 -0
  37. data/app/models/good_job/batch.rb +17 -16
  38. data/app/models/good_job/batch_record.rb +10 -8
  39. data/app/models/good_job/concurrency_claim.rb +3 -0
  40. data/app/models/good_job/cron_entry.rb +10 -11
  41. data/app/models/good_job/discrete_execution.rb +1 -0
  42. data/app/models/good_job/execution.rb +8 -3
  43. data/app/models/good_job/execution_result.rb +1 -0
  44. data/app/models/good_job/i18n_config.rb +1 -0
  45. data/app/models/good_job/job/lockable.rb +68 -42
  46. data/app/models/good_job/job.rb +22 -16
  47. data/app/models/good_job/process.rb +5 -5
  48. data/app/models/good_job/setting.rb +5 -2
  49. data/app/views/good_job/executions/show.html.erb +11 -0
  50. data/app/views/good_job/jobs/_executions.erb +2 -6
  51. data/app/views/good_job/jobs/show.html.erb +1 -1
  52. data/config/brakeman.ignore +9 -9
  53. data/config/locales/pt-BR.yml +25 -23
  54. data/config/routes.rb +3 -1
  55. data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
  56. data/lib/generators/good_job/install_generator.rb +1 -0
  57. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -0
  58. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +1 -0
  59. data/lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb +12 -0
  60. data/lib/generators/good_job/update_generator.rb +1 -0
  61. data/lib/good_job/active_job_extensions/batches.rb +1 -0
  62. data/lib/good_job/active_job_extensions/concurrency.rb +1 -0
  63. data/lib/good_job/active_job_extensions/interrupt_errors.rb +1 -0
  64. data/lib/good_job/active_job_extensions/labels.rb +1 -0
  65. data/lib/good_job/active_job_extensions/notify_options.rb +1 -0
  66. data/lib/good_job/adapter/inline_buffer.rb +1 -0
  67. data/lib/good_job/adapter.rb +52 -59
  68. data/lib/good_job/bulk.rb +1 -0
  69. data/lib/good_job/callable.rb +1 -0
  70. data/lib/good_job/capsule.rb +12 -8
  71. data/lib/good_job/capsule_tracker.rb +35 -17
  72. data/lib/good_job/cleanup_tracker.rb +1 -0
  73. data/lib/good_job/cli.rb +5 -0
  74. data/lib/good_job/configuration/validator.rb +1 -0
  75. data/lib/good_job/configuration.rb +13 -0
  76. data/lib/good_job/cron_manager.rb +6 -6
  77. data/lib/good_job/current_thread.rb +2 -1
  78. data/lib/good_job/daemon.rb +1 -0
  79. data/lib/good_job/dependencies.rb +1 -0
  80. data/lib/good_job/engine.rb +3 -1
  81. data/lib/good_job/instance_registry.rb +48 -0
  82. data/lib/good_job/interrupt_error.rb +1 -0
  83. data/lib/good_job/interrupted_error.rb +1 -0
  84. data/lib/good_job/job_performer/metrics.rb +1 -0
  85. data/lib/good_job/job_performer.rb +7 -5
  86. data/lib/good_job/lifecycle_hooks.rb +1 -0
  87. data/lib/good_job/log_subscriber.rb +1 -0
  88. data/lib/good_job/multi_scheduler.rb +1 -0
  89. data/lib/good_job/notifier/process_heartbeat.rb +1 -0
  90. data/lib/good_job/notifier.rb +6 -6
  91. data/lib/good_job/poller.rb +5 -6
  92. data/lib/good_job/probe_server/cluster_healthcheck_middleware.rb +1 -0
  93. data/lib/good_job/probe_server/healthcheck_middleware.rb +1 -0
  94. data/lib/good_job/probe_server/not_found_app.rb +1 -0
  95. data/lib/good_job/probe_server/simple_handler.rb +51 -11
  96. data/lib/good_job/probe_server/webrick_handler.rb +3 -0
  97. data/lib/good_job/probe_server.rb +3 -0
  98. data/lib/good_job/ractors.rb +3 -0
  99. data/lib/good_job/safe_state.rb +1 -0
  100. data/lib/good_job/scheduler.rb +4 -6
  101. data/lib/good_job/sd_notify.rb +1 -0
  102. data/lib/good_job/shared_executor.rb +5 -6
  103. data/lib/good_job/subprocess.rb +1 -0
  104. data/lib/good_job/supervisor.rb +22 -15
  105. data/lib/good_job/systemd_service.rb +2 -0
  106. data/lib/good_job/thread_status.rb +1 -0
  107. data/lib/good_job/version.rb +2 -1
  108. data/lib/good_job.rb +60 -23
  109. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ce4bd5cfbb7e4fcc44f90099bc1aa892ef59e1294c69eac5d4279c3968db9c0
4
- data.tar.gz: '093ba2cc69d8d51f38bbf4cd93d9bf97aa65d995768f00a6b0d27dbe1aacdd08'
3
+ metadata.gz: 6973f64ae5640b5157d72d26438c7bb155072f07a98829461324b548f0092a19
4
+ data.tar.gz: 5b9dbd764aa128a9b54797e1f0abe0912942a391d656070ca86f4302041fed33
5
5
  SHA512:
6
- metadata.gz: bcbcad67274883a3cb3d4405d7d43015c5c154241dab9c1d136ccbcae7ea53bf9e6606d4713761b7c565826487f042c0e869ecc14eafacfa46ce3bfb9de6dc26
7
- data.tar.gz: 56b86db61ba722ebbe770f0f0be2f20eaea6a132af9a3d9e5f688a2c2138d3782d92a3a425ff47b0e1881539c7bfe8135c18444b887665105edc385d3f3aa3a7
6
+ metadata.gz: e9a5a767e63afdf414c61d9437486d2670d14bc37f006963a5f4c2f13a6b23805fb9fb70e51068d67ee6f7f3133463ba201798a0464260fc0cba09fa319cbeb3
7
+ data.tar.gz: 49c3e706bc73a2715993d38a0713ec736864c7fe9a434461db87092b0ba20c0900043e324887d17c5d40e14d0dd3df0c304caae0c77f912e952a8b66c65beff8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.24.1](https://github.com/bensheldon/good_job/tree/v4.24.1) (2026-11-03)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.24.0...v4.24.1)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Look up cron settings once, not per entry, in the primary nav \(36 ms → 0.75 ms at 100 entries\) [\#1906](https://github.com/bensheldon/good_job/pull/1906) ([czj](https://github.com/czj))
10
+ - Sort dashboard chart legends alphabetically [\#1903](https://github.com/bensheldon/good_job/pull/1903) ([6temes](https://github.com/6temes))
11
+
12
+ **Closed issues:**
13
+
14
+ - Unique constraint violation messages in postgres error logs [\#1536](https://github.com/bensheldon/good_job/issues/1536)
15
+
16
+ **Merged pull requests:**
17
+
18
+ - Document expected duplicate key errors from cron in Postgres logs [\#1908](https://github.com/bensheldon/good_job/pull/1908) ([rafayqayyum](https://github.com/rafayqayyum))
19
+ - Bump github/codeql-action from 4.38.2 to 4.39.1 [\#1907](https://github.com/bensheldon/good_job/pull/1907) ([dependabot[bot]](https://github.com/apps/dependabot))
20
+
21
+ ## [v4.24.0](https://github.com/bensheldon/good_job/tree/v4.24.0) (2026-10-29)
22
+
23
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.1...v4.24.0)
24
+
25
+ **Implemented enhancements:**
26
+
27
+ - Add a separate retention period for discarded jobs [\#1897](https://github.com/bensheldon/good_job/pull/1897) ([IslamElsayed](https://github.com/IslamElsayed))
28
+
29
+ **Fixed bugs:**
30
+
31
+ - Prevent idle or malformed connections from stalling or stopping the probe server [\#1902](https://github.com/bensheldon/good_job/pull/1902) ([bensheldon](https://github.com/bensheldon))
32
+ - Validate advisory lock SQL identifiers and harden dashboard filter params [\#1901](https://github.com/bensheldon/good_job/pull/1901) ([bensheldon](https://github.com/bensheldon))
33
+ - Don't retain or report schedulers replaced by a capsule restart [\#1894](https://github.com/bensheldon/good_job/pull/1894) ([bensheldon](https://github.com/bensheldon))
34
+
35
+ **Closed issues:**
36
+
37
+ - \[feature request\] Cleanup older discarded jobs [\#1597](https://github.com/bensheldon/good_job/issues/1597)
38
+
39
+ **Merged pull requests:**
40
+
41
+ - Restructure code so it type-checks under Sorbet without runtime assertions [\#1899](https://github.com/bensheldon/good_job/pull/1899) ([bensheldon](https://github.com/bensheldon))
42
+ - Upgrade development/demo Ruby to 4.0.9 and update dependencies [\#1898](https://github.com/bensheldon/good_job/pull/1898) ([bensheldon](https://github.com/bensheldon))
43
+ - Fix flaky JRuby FrozenError by preloading record schemas in specs [\#1896](https://github.com/bensheldon/good_job/pull/1896) ([bensheldon](https://github.com/bensheldon))
44
+ - Demo: shorten database connection lifetimes [\#1895](https://github.com/bensheldon/good_job/pull/1895) ([bensheldon](https://github.com/bensheldon))
45
+
46
+ ## [v4.23.1](https://github.com/bensheldon/good_job/tree/v4.23.1) (2026-10-27)
47
+
48
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.0...v4.23.1)
49
+
50
+ **Fixed bugs:**
51
+
52
+ - Release abandoned job claims after database failures [\#1887](https://github.com/bensheldon/good_job/pull/1887) ([ollym](https://github.com/ollym))
53
+
54
+ ## [v4.23.0](https://github.com/bensheldon/good_job/tree/v4.23.0) (2026-10-27)
55
+
56
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.1...v4.23.0)
57
+
58
+ **Implemented enhancements:**
59
+
60
+ - Filter sensitive job arguments in the Dashboard [\#1881](https://github.com/bensheldon/good_job/pull/1881) ([bogdan](https://github.com/bogdan))
61
+
62
+ **Fixed bugs:**
63
+
64
+ - Respect enable\_listen\_notify when promoting concurrency claims [\#1892](https://github.com/bensheldon/good_job/pull/1892) ([eidam](https://github.com/eidam))
65
+ - Downgrade lock\_type before releasing advisory lock [\#1883](https://github.com/bensheldon/good_job/pull/1883) ([jmsanders](https://github.com/jmsanders))
66
+
67
+ **Closed issues:**
68
+
69
+ - ConcurrencyClaim sends NOTIFY even when enable\_listen\_notify is false [\#1891](https://github.com/bensheldon/good_job/issues/1891)
70
+
71
+ **Merged pull requests:**
72
+
73
+ - Keep JRuby platform in Gemfile.lock when Bundler re-resolves [\#1893](https://github.com/bensheldon/good_job/pull/1893) ([bensheldon](https://github.com/bensheldon))
74
+ - Make integration specs robust under database load; fix mixed lock strategy specs [\#1890](https://github.com/bensheldon/good_job/pull/1890) ([bensheldon](https://github.com/bensheldon))
75
+ - Pin CI test runners to ubuntu-24.04 [\#1888](https://github.com/bensheldon/good_job/pull/1888) ([bensheldon](https://github.com/bensheldon))
76
+ - Require the concurrent-ruby classes GoodJob uses [\#1885](https://github.com/bensheldon/good_job/pull/1885) ([czj](https://github.com/czj))
77
+ - Add index\_good\_job\_executions\_on\_created\_at [\#1884](https://github.com/bensheldon/good_job/pull/1884) ([clinejj](https://github.com/clinejj))
78
+ - Demo: cap YJIT memory and shrink the Rack::Attack store [\#1880](https://github.com/bensheldon/good_job/pull/1880) ([bensheldon](https://github.com/bensheldon))
79
+ - Demo: enable rbtrace in Puma workers for heap dumps; add sheap [\#1879](https://github.com/bensheldon/good_job/pull/1879) ([bensheldon](https://github.com/bensheldon))
80
+ - Demo: warm up Puma before forking, add Thruster, raise DB pool [\#1878](https://github.com/bensheldon/good_job/pull/1878) ([bensheldon](https://github.com/bensheldon))
81
+ - Return 404 for GET /jobs/mass\_update instead of redirecting [\#1877](https://github.com/bensheldon/good_job/pull/1877) ([bensheldon](https://github.com/bensheldon))
82
+ - Load execution full traces and serialized params on demand [\#1876](https://github.com/bensheldon/good_job/pull/1876) ([bensheldon](https://github.com/bensheldon))
83
+ - Add specs for the demo's Rack::Attack rules [\#1875](https://github.com/bensheldon/good_job/pull/1875) ([bensheldon](https://github.com/bensheldon))
84
+ - Fix demo boot: don't require rails/all in bin/rails [\#1874](https://github.com/bensheldon/good_job/pull/1874) ([bensheldon](https://github.com/bensheldon))
85
+ - Reduce demo app memory and crawler load [\#1873](https://github.com/bensheldon/good_job/pull/1873) ([bensheldon](https://github.com/bensheldon))
86
+ - Fix JRuby flake from inspect in wait\_until predicate expectations [\#1872](https://github.com/bensheldon/good_job/pull/1872) ([bensheldon](https://github.com/bensheldon))
87
+ - Remove Adapter::RUNTIMES so adapters can be garbage collected [\#1870](https://github.com/bensheldon/good_job/pull/1870) ([bensheldon](https://github.com/bensheldon))
88
+ - Add GoodJob.prune to release shut down capsules, schedulers, and notifiers [\#1869](https://github.com/bensheldon/good_job/pull/1869) ([bensheldon](https://github.com/bensheldon))
89
+ - Use resolve\_\[property\] naming for CronEntry configuration resolvers [\#1868](https://github.com/bensheldon/good_job/pull/1868) ([bensheldon](https://github.com/bensheldon))
90
+ - Fix inconsistencies in the Brazilian Portuguese locale [\#1867](https://github.com/bensheldon/good_job/pull/1867) ([tiagoamaro](https://github.com/tiagoamaro))
91
+
3
92
  ## [v4.22.1](https://github.com/bensheldon/good_job/tree/v4.22.1) (2026-10-13)
4
93
 
5
94
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.0...v4.22.1)
data/README.md CHANGED
@@ -44,6 +44,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
44
44
  - [Dashboard](#dashboard)
45
45
  - [API-only Rails applications](#api-only-rails-applications)
46
46
  - [Live polling](#live-polling)
47
+ - [Filtering sensitive arguments](#filtering-sensitive-arguments)
47
48
  - [Extending dashboard views](#extending-dashboard-views)
48
49
  - [Job priority](#job-priority)
49
50
  - [Concurrency controls](#concurrency-controls)
@@ -235,7 +236,8 @@ Usage:
235
236
  good_job cleanup_preserved_jobs
236
237
 
237
238
  Options:
238
- [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 1209600 (14 days))
239
+ [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 1209600 (14 days))
240
+ [--discarded-before-seconds-ago=SECONDS] # Destroy discarded records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_DISCARDED_JOBS_BEFORE_SECONDS_AGO, default: same as --before-seconds-ago)
239
241
 
240
242
  Manually destroys preserved job records.
241
243
 
@@ -316,6 +318,7 @@ Available configuration options are:
316
318
  - `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
317
319
  - `cleanup_discarded_jobs` (boolean) whether to destroy discarded jobs when cleaning up preserved jobs using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `true`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS`.
318
320
  - `cleanup_preserved_jobs_before_seconds_ago` (integer) number of seconds to preserve jobs when using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `1209600` (14 days). Can also be set with the environment variable `GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO`.
321
+ - `cleanup_discarded_jobs_before_seconds_ago` (integer) number of seconds to preserve discarded jobs, when they should be kept longer (or shorter) than other jobs. Has no effect when `cleanup_discarded_jobs` is `false`. Defaults to `nil`, which uses `cleanup_preserved_jobs_before_seconds_ago`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS_BEFORE_SECONDS_AGO`.
319
322
  - `cleanup_interval_jobs` (integer) Number of jobs a Scheduler will execute before cleaning up preserved jobs. Defaults to `1000`. Disable with `false`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_JOBS` and disabled with `0`).
320
323
  - `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `600` (10 minutes). Disable with `false`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS` and disabled with `0`).
321
324
  - `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).
@@ -479,6 +482,15 @@ end
479
482
 
480
483
  The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
481
484
 
485
+ #### Filtering sensitive arguments
486
+
487
+ The Dashboard hides job arguments the same way Rails hides them from logs:
488
+
489
+ - Hash values whose keys match `Rails.application.config.filter_parameters` (e.g. `password:`) are displayed as `[FILTERED]`.
490
+ - All arguments are displayed as `[FILTERED]` for jobs with `log_arguments` disabled, either for every job with `config.active_job.log_arguments = false` or per job class with `self.log_arguments = false`.
491
+
492
+ Filtering only applies to how arguments are displayed; arguments are still stored unfiltered in the database and are used when the job is performed or retried.
493
+
482
494
  #### Extending dashboard views
483
495
 
484
496
  GoodJob exposes some views that are intended to be overridden by placing views in your application:
@@ -793,6 +805,8 @@ GoodJob can enqueue Active Job jobs on a recurring basis that can be used as a r
793
805
 
794
806
  Cron-style jobs can be enequeued by any GoodJob process (e.g., CLI or `:async` execution mode) that has `config.good_job.enable_cron` set to `true`. Enabling cron on multiple processes will not enqueue duplicate jobs; GoodJob's cron uses unique indexes to ensure that only a single job is enqueued for a given time interval. In order for this to work, GoodJob must preserve cron-created job records; these records will be automatically deleted like any other preserved record.
795
807
 
808
+ When cron is enabled on multiple processes, each process attempts to enqueue every scheduled job and all but one of the inserts fail on the unique index. GoodJob rescues these failures, but PostgreSQL still writes them to its own log as `duplicate key value violates unique constraint "index_good_jobs_on_cron_key_and_cron_at_cond"`. These errors are expected and harmless. If they are too noisy, enable cron on a single process instead, at the cost of no redundancy if that process stops.
809
+
796
810
  Cron-format is parsed by the [`fugit`](https://github.com/floraison/fugit) gem, which has support for seconds-level resolution (e.g. `* * * * * *`) and natural language parsing (e.g. `every second`).
797
811
 
798
812
  If you use the [Dashboard](#dashboard) the scheduled tasks can be viewed in the 'cron' menu. In this view you can also disable a task or run/enqueue a task immediately.
@@ -1215,6 +1229,8 @@ Note that when running jobs in `:inline` execution mode, `GoodJob.current_thread
1215
1229
 
1216
1230
  Jobs will be automatically retried if the process is interrupted while performing a job and the job is unable to finish before the timeout or as the result of a `SIGKILL` or power failure. The interrupted execution's error record will show `GoodJob::InterruptedError` to distinguish it from the rescuable `GoodJob::InterruptError` that is raised when the job is retried.
1217
1231
 
1232
+ With `:skiplocked` or `:hybrid` locking, a worker releases its claim even if recording the job's completion fails. If the database connection is unavailable during cleanup, the worker waits and retries every five seconds until it can release the claim, GoodJob begins shutting down, or the worker is forcibly terminated. If the worker stops retrying, the claim is released when the exited process's record is cleaned up. The job can then be recovered as an interrupted execution, which may run it again; external effects should be idempotent.
1233
+
1218
1234
  If you need more control over interrupt-caused retries, include the `GoodJob::ActiveJobExtensions::InterruptErrors` extension in your job class. When an interrupted job is retried, the extension will raise a `GoodJob::InterruptError` exception within the job, which allows you to use Active Job's `retry_on` and `discard_on` to control the behavior of the job.
1219
1235
 
1220
1236
  ```ruby
@@ -1588,6 +1604,7 @@ GoodJob will automatically delete preserved job records after 14 days. The reten
1588
1604
 
1589
1605
  ```ruby
1590
1606
  config.good_job.cleanup_preserved_jobs_before_seconds_ago = 14.days
1607
+ config.good_job.cleanup_discarded_jobs_before_seconds_ago = 90.days # Optional: keep discarded jobs longer than the others.
1591
1608
  config.good_job.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.
1592
1609
  config.good_job.cleanup_interval_seconds = 10.minutes # Number of seconds between deletion sweeps.
1593
1610
  ```
@@ -1599,6 +1616,7 @@ It is also possible to manually trigger a cleanup of preserved job records:
1599
1616
  ```ruby
1600
1617
  GoodJob.cleanup_preserved_jobs # Will use default retention period
1601
1618
  GoodJob.cleanup_preserved_jobs(older_than: 7.days) # custom retention period
1619
+ GoodJob.cleanup_preserved_jobs(older_than: 7.days, discarded_older_than: 90.days) # keep discarded jobs longer
1602
1620
  ```
1603
1621
 
1604
1622
  - For example, using the `good_job` command-line utility:
@@ -1644,7 +1662,7 @@ end
1644
1662
  GOOD_JOB_LOCK_STRATEGY=skiplocked
1645
1663
  ```
1646
1664
 
1647
- All three strategies (`:advisory`, `:skiplocked`, `:hybrid`) can coexist safely during a rolling deploy — each strategy excludes jobs that are already locked by another worker regardless of which strategy that worker uses.
1665
+ During a rolling deploy, `:hybrid` workers can safely run alongside either `:advisory` or `:skiplocked` workers. But `:advisory` and `:skiplocked` workers must not run at the same time, because neither sees the other's locks and the same job may be performed twice. To move from `:advisory` to `:skiplocked`, deploy `:hybrid` first, wait until no `:advisory` workers remain, and then deploy `:skiplocked`.
1648
1666
 
1649
1667
  #### PgBouncer configuration
1650
1668
 
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -6,7 +7,7 @@ module GoodJob
6
7
  # expression over +good_job_executions+, the aggregate that summarizes it per
7
8
  # time bucket, and their titles.
8
9
  class Base
9
- def expression = self.class::EXPRESSION
10
+ def expression = self.class.const_get(:EXPRESSION)
10
11
 
11
12
  def to_arel = Arel.sql(expression)
12
13
 
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -48,7 +49,7 @@ module GoodJob
48
49
 
49
50
  executions_data = GoodJob::Job.connection_pool.with_connection { |conn| conn.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Chart", binds) }
50
51
 
51
- job_names = executions_data.reject { |d| d['value'].nil? }.map { |d| d['job_class'] || BaseFilter::EMPTY }.uniq
52
+ job_names = executions_data.reject { |d| d['value'].nil? }.map { |d| d['job_class'] || BaseFilter::EMPTY }.uniq.sort
52
53
  timestamp_values = []
53
54
  timestamps = []
54
55
  jobs_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -281,8 +282,9 @@ module GoodJob
281
282
  components = match.captures
282
283
  components[-1] ||= "0"
283
284
  numeric_components = components.map { |component| Integer(component, 10) }
284
- local_time = Time.utc(*numeric_components)
285
- normalized_value = format("%04d-%02d-%02dT%02d:%02d:%02d", *numeric_components)
285
+ year, month, day, hour, minute, second = numeric_components
286
+ local_time = Time.utc(year, month, day, hour, minute, second)
287
+ normalized_value = format("%04d-%02d-%02dT%02d:%02d:%02d", year, month, day, hour, minute, second)
286
288
  return unless local_time.strftime("%Y-%m-%dT%H:%M:%S") == normalized_value
287
289
 
288
290
  periods = @local_time_zone.tzinfo.periods_for_local(local_time)
@@ -291,7 +293,7 @@ module GoodJob
291
293
  # A local input cannot carry an offset. Include both repeated fall-back occurrences
292
294
  # by resolving starts to the earlier instant and ends to the later instant.
293
295
  instants = periods.map do |period|
294
- (local_time - period.utc_total_offset).in_time_zone
296
+ local_time.ago(period.utc_total_offset).in_time_zone
295
297
  end
296
298
  parameter_key == "chart_end" ? instants.max : instants.min
297
299
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -47,7 +48,7 @@ module GoodJob
47
48
 
48
49
  executions_data = GoodJob::Job.connection_pool.with_connection { |conn| conn.exec_query(GoodJob::Job.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", binds) }
49
50
 
50
- queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq
51
+ queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq.sort
51
52
  labels = []
52
53
  queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
53
54
  labels << timestamp.in_time_zone.strftime('%H:%M')
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -19,14 +20,16 @@ module GoodJob
19
20
  policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
20
21
  end
21
22
 
22
- before_action do
23
- next if request.content_security_policy_nonce_generator
23
+ before_action :set_content_security_policy_nonce_generator
24
+
25
+ private
26
+
27
+ def set_content_security_policy_nonce_generator
28
+ return if request.content_security_policy_nonce_generator
24
29
 
25
30
  request.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
26
31
  end
27
32
 
28
- private
29
-
30
33
  def default_url_options(options = {})
31
34
  { locale: I18n.locale }.merge(options)
32
35
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -0,0 +1,12 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module GoodJob
5
+ # Renders an execution's heavier details on demand so they aren't included in every job page.
6
+ class ExecutionsController < GoodJob::ApplicationController
7
+ def show
8
+ @execution = Execution.find_by!(id: params[:id], active_job_id: params[:job_id])
9
+ render layout: false
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -6,7 +7,7 @@ module GoodJob
6
7
  skip_after_action :verify_same_origin_request, raise: false
7
8
 
8
9
  def self.asset_path(*path)
9
- Engine.root.join("app/frontend/good_job", *path)
10
+ path.reduce(Engine.root.join("app/frontend/good_job")) { |asset_path, segment| asset_path.join(segment) }
10
11
  end
11
12
 
12
13
  STATIC_ASSETS = {
@@ -38,9 +39,7 @@ module GoodJob
38
39
  end.merge(MODULE_OVERRIDES)
39
40
  end
40
41
 
41
- before_action do
42
- expires_in 1.year, public: true
43
- end
42
+ before_action :set_cache_headers
44
43
 
45
44
  def static
46
45
  render file: STATIC_ASSETS.dig(params[:format]&.to_sym, params[:id]&.to_sym) || raise(ActionController::RoutingError, 'Not Found')
@@ -51,5 +50,11 @@ module GoodJob
51
50
 
52
51
  render file: self.class.js_modules[params[:id]&.to_sym] || raise(ActionController::RoutingError, 'Not Found')
53
52
  end
53
+
54
+ private
55
+
56
+ def set_cache_headers
57
+ expires_in 1.year, public: true
58
+ end
54
59
  end
55
60
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -7,7 +8,9 @@ module GoodJob
7
8
  batches_count = GoodJob::BatchRecord.all.size
8
9
  cron_entries = GoodJob::CronEntry.all
9
10
  cron_entries_count = cron_entries.size
10
- cron_entries_enabled_count = cron_entries.count(&:enabled?)
11
+ cron_entries_enabled_count = GoodJob::Setting.cron_keys_enabled(
12
+ cron_entries.map { |entry| [entry.key, entry.enabled_by_default?] }
13
+ ).values.count(true)
11
14
  cron_entries_paused_count = cron_entries_count - cron_entries_enabled_count
12
15
  pauses_count = GoodJob::Setting.paused.values.sum(&:count)
13
16
  processes_count = GoodJob::Process.active.count
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -15,7 +16,7 @@ module GoodJob
15
16
  def records
16
17
  @_records ||= begin
17
18
  after_id = params[:after_id]
18
- after_at = params[:after_at].present? ? Time.zone.parse(params[:after_at]) : nil
19
+ after_at = parse_after_at(params[:after_at])
19
20
  limit = params.fetch(:limit, DEFAULT_LIMIT)
20
21
 
21
22
  query_for_records.display_all(
@@ -88,6 +89,14 @@ module GoodJob
88
89
 
89
90
  private
90
91
 
92
+ def parse_after_at(value)
93
+ return unless value.is_a?(String) && value.present?
94
+
95
+ Time.zone.parse(value)
96
+ rescue ArgumentError
97
+ nil
98
+ end
99
+
91
100
  def query_for_records
92
101
  raise NotImplementedError
93
102
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -92,7 +93,9 @@ module GoodJob
92
93
  end
93
94
 
94
95
  def filter_by_search_query(query)
95
- search_query = query&.strip
96
+ return {} unless query.is_a?(String)
97
+
98
+ search_query = query.strip
96
99
  return {} if search_query.blank?
97
100
 
98
101
  if query_is_uuid_and_job_exists?(search_query)
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -30,6 +31,10 @@ module GoodJob
30
31
  #
31
32
  # @example
32
33
  # GoodJob::AdvisoryLockable.hash_function = "sha256"
34
+ # pgcrypto digest() types; "md5", "hashtext", and "uuid_v5" have dedicated branches in +advisory_lock_bigint_sql+.
35
+ DIGEST_HASH_FUNCTIONS = %w[sha1 sha224 sha256 sha384 sha512].freeze
36
+ ADVISORY_LOCK_FUNCTION_PATTERN = /\Apg_(try_)?advisory_(xact_)?(un)?lock(_shared)?\z/
37
+
33
38
  class << self
34
39
  def hash_function=(value)
35
40
  @_hash_function = value
@@ -386,7 +391,7 @@ module GoodJob
386
391
  cte_type = supports_cte_materialization_specifiers? ? :MATERIALIZED : :""
387
392
  composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::UnaryOperation.new(cte_type, cte_query.arel))
388
393
 
389
- lock_condition = "#{function}(#{advisory_lock_bigint_sql("#{_quoted_table_name_string} || '-' || #{adapter_class.quote_table_name(cte_table.name)}.#{adapter_class.quote_column_name(column)}::text")})"
394
+ lock_condition = "#{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql("#{_quoted_table_name_string} || '-' || #{adapter_class.quote_table_name(cte_table.name)}.#{adapter_class.quote_column_name(column)}::text")})"
390
395
  query = cte_table.project(cte_table[:id])
391
396
  .with(composed_cte)
392
397
  .where(defined?(Arel::Nodes::BoundSqlLiteral) ? Arel::Nodes::BoundSqlLiteral.new(lock_condition, [], {}) : Arel::Nodes::SqlLiteral.new(lock_condition))
@@ -414,11 +419,11 @@ module GoodJob
414
419
  def advisory_lock_key(key, function: advisory_lockable_function, connection: nil)
415
420
  query = if function.include? "_try_"
416
421
  <<~SQL.squish
417
- SELECT #{function}(#{advisory_lock_bigint_sql('$1::text')}) AS locked
422
+ SELECT #{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql('$1::text')}) AS locked
418
423
  SQL
419
424
  else
420
425
  <<~SQL.squish
421
- SELECT #{function}(#{advisory_lock_bigint_sql('$1::text')})::text AS locked
426
+ SELECT #{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql('$1::text')})::text AS locked
422
427
  SQL
423
428
  end
424
429
 
@@ -522,7 +527,7 @@ module GoodJob
522
527
  raise ArgumentError, "No unlock function provided" if function.blank?
523
528
 
524
529
  query = <<~SQL.squish
525
- SELECT #{function}(#{advisory_lock_bigint_sql('$1::text')}) AS unlocked
530
+ SELECT #{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql('$1::text')}) AS unlocked
526
531
  SQL
527
532
  binds = [
528
533
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
@@ -608,10 +613,23 @@ module GoodJob
608
613
  "('x' || substr(replace(uuid_generate_v5('6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid, (#{value_sql})::text)::text, '-', ''), 1, 16))::bit(64)::bigint"
609
614
  else
610
615
  # pgcrypto's digest() supports sha1, sha224, sha256, sha384, sha512
611
- "('x' || substr(encode(digest((#{value_sql})::text, '#{GoodJob::AdvisoryLockable.hash_function}'), 'hex'), 1, 16))::bit(64)::bigint"
616
+ digest_type = GoodJob::AdvisoryLockable.hash_function.to_s.downcase
617
+ raise ArgumentError, "Unsupported advisory lock hash function: #{digest_type.inspect}" unless DIGEST_HASH_FUNCTIONS.include?(digest_type)
618
+
619
+ "('x' || substr(encode(digest((#{value_sql})::text, '#{digest_type}'), 'hex'), 1, 16))::bit(64)::bigint"
612
620
  end
613
621
  end
614
622
 
623
+ # Validates a Postgres advisory lock function name before it is interpolated into SQL.
624
+ # @param function [String, Symbol]
625
+ # @return [String]
626
+ def advisory_lock_function_sql(function)
627
+ function = function.to_s
628
+ raise ArgumentError, "Unsupported advisory lock function: #{function.inspect}" unless ADVISORY_LOCK_FUNCTION_PATTERN.match?(function)
629
+
630
+ function
631
+ end
632
+
615
633
  # Extracts the upper 32 bits of the advisory lock bigint, used as +classid+ in pg_locks.
616
634
  def advisory_lock_classid_sql(value_sql)
617
635
  "substring((#{advisory_lock_bigint_sql(value_sql)})::bit(64) from 1 for 32)::bit(32)::int"
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module GoodJob
5
+ # Hides sensitive Active Job arguments when displaying Job/Execution records in the Dashboard.
6
+ module FilteredArguments
7
+ # Active Job arguments with sensitive values replaced by +[FILTERED]+.
8
+ # Every argument is filtered when the job class has +log_arguments+ disabled
9
+ # (e.g. +config.active_job.log_arguments = false+); otherwise hash values whose
10
+ # keys match +Rails.application.config.filter_parameters+ are filtered.
11
+ # @return [Array]
12
+ def display_arguments
13
+ arguments = serialized_params.fetch("arguments", [])
14
+ return arguments.map { ActiveSupport::ParameterFilter::FILTERED } unless log_arguments?
15
+
16
+ parameter_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
17
+ parameter_filter.filter("arguments" => arguments)["arguments"]
18
+ end
19
+
20
+ private
21
+
22
+ def log_arguments?
23
+ active_job_class = job_class&.safe_constantize
24
+ active_job_class = ActiveJob::Base unless active_job_class.respond_to?(:log_arguments?)
25
+ active_job_class.log_arguments?
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: ignore
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob