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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -0
- data/README.md +20 -2
- data/app/charts/good_job/base_chart.rb +1 -0
- data/app/charts/good_job/latency_metric/base.rb +2 -1
- data/app/charts/good_job/latency_metric/execution.rb +1 -0
- data/app/charts/good_job/latency_metric/queue.rb +1 -0
- data/app/charts/good_job/latency_metric/total.rb +1 -0
- data/app/charts/good_job/latency_metric.rb +1 -0
- data/app/charts/good_job/performance_index_chart.rb +2 -1
- data/app/charts/good_job/performance_range.rb +5 -3
- data/app/charts/good_job/performance_show_chart.rb +1 -0
- data/app/charts/good_job/scheduled_by_queue_chart.rb +2 -1
- data/app/controllers/good_job/application_controller.rb +7 -4
- data/app/controllers/good_job/batches_controller.rb +1 -0
- data/app/controllers/good_job/cleaner_controller.rb +1 -0
- data/app/controllers/good_job/cron_entries_controller.rb +1 -0
- data/app/controllers/good_job/executions_controller.rb +12 -0
- data/app/controllers/good_job/frontends_controller.rb +9 -4
- data/app/controllers/good_job/jobs_controller.rb +1 -0
- data/app/controllers/good_job/metrics_controller.rb +4 -1
- data/app/controllers/good_job/pauses_controller.rb +1 -0
- data/app/controllers/good_job/performance_controller.rb +1 -0
- data/app/controllers/good_job/processes_controller.rb +1 -0
- data/app/filters/good_job/base_filter.rb +10 -1
- data/app/filters/good_job/batches_filter.rb +1 -0
- data/app/filters/good_job/jobs_filter.rb +4 -1
- data/app/helpers/good_job/application_helper.rb +1 -0
- data/app/helpers/good_job/icons_helper.rb +1 -0
- data/app/models/concerns/good_job/advisory_lockable.rb +23 -5
- data/app/models/concerns/good_job/error_events.rb +1 -0
- data/app/models/concerns/good_job/filterable.rb +1 -0
- data/app/models/concerns/good_job/filtered_arguments.rb +28 -0
- data/app/models/concerns/good_job/reportable.rb +1 -0
- data/app/models/good_job/active_record_parent_class.rb +1 -0
- data/app/models/good_job/base_record.rb +1 -0
- data/app/models/good_job/batch.rb +17 -16
- data/app/models/good_job/batch_record.rb +10 -8
- data/app/models/good_job/concurrency_claim.rb +3 -0
- data/app/models/good_job/cron_entry.rb +10 -11
- data/app/models/good_job/discrete_execution.rb +1 -0
- data/app/models/good_job/execution.rb +8 -3
- data/app/models/good_job/execution_result.rb +1 -0
- data/app/models/good_job/i18n_config.rb +1 -0
- data/app/models/good_job/job/lockable.rb +68 -42
- data/app/models/good_job/job.rb +22 -16
- data/app/models/good_job/process.rb +5 -5
- data/app/models/good_job/setting.rb +5 -2
- data/app/views/good_job/executions/show.html.erb +11 -0
- data/app/views/good_job/jobs/_executions.erb +2 -6
- data/app/views/good_job/jobs/show.html.erb +1 -1
- data/config/brakeman.ignore +9 -9
- data/config/locales/pt-BR.yml +25 -23
- data/config/routes.rb +3 -1
- 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/install/migrations/create_good_jobs.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb +12 -0
- data/lib/generators/good_job/update_generator.rb +1 -0
- data/lib/good_job/active_job_extensions/batches.rb +1 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +1 -0
- data/lib/good_job/active_job_extensions/interrupt_errors.rb +1 -0
- data/lib/good_job/active_job_extensions/labels.rb +1 -0
- data/lib/good_job/active_job_extensions/notify_options.rb +1 -0
- data/lib/good_job/adapter/inline_buffer.rb +1 -0
- data/lib/good_job/adapter.rb +52 -59
- data/lib/good_job/bulk.rb +1 -0
- data/lib/good_job/callable.rb +1 -0
- data/lib/good_job/capsule.rb +12 -8
- data/lib/good_job/capsule_tracker.rb +35 -17
- data/lib/good_job/cleanup_tracker.rb +1 -0
- data/lib/good_job/cli.rb +5 -0
- data/lib/good_job/configuration/validator.rb +1 -0
- data/lib/good_job/configuration.rb +13 -0
- data/lib/good_job/cron_manager.rb +6 -6
- data/lib/good_job/current_thread.rb +2 -1
- data/lib/good_job/daemon.rb +1 -0
- data/lib/good_job/dependencies.rb +1 -0
- data/lib/good_job/engine.rb +3 -1
- data/lib/good_job/instance_registry.rb +48 -0
- data/lib/good_job/interrupt_error.rb +1 -0
- data/lib/good_job/interrupted_error.rb +1 -0
- data/lib/good_job/job_performer/metrics.rb +1 -0
- data/lib/good_job/job_performer.rb +7 -5
- data/lib/good_job/lifecycle_hooks.rb +1 -0
- data/lib/good_job/log_subscriber.rb +1 -0
- data/lib/good_job/multi_scheduler.rb +1 -0
- data/lib/good_job/notifier/process_heartbeat.rb +1 -0
- data/lib/good_job/notifier.rb +6 -6
- data/lib/good_job/poller.rb +5 -6
- data/lib/good_job/probe_server/cluster_healthcheck_middleware.rb +1 -0
- data/lib/good_job/probe_server/healthcheck_middleware.rb +1 -0
- data/lib/good_job/probe_server/not_found_app.rb +1 -0
- data/lib/good_job/probe_server/simple_handler.rb +51 -11
- data/lib/good_job/probe_server/webrick_handler.rb +3 -0
- data/lib/good_job/probe_server.rb +3 -0
- data/lib/good_job/ractors.rb +3 -0
- data/lib/good_job/safe_state.rb +1 -0
- data/lib/good_job/scheduler.rb +4 -6
- data/lib/good_job/sd_notify.rb +1 -0
- data/lib/good_job/shared_executor.rb +5 -6
- data/lib/good_job/subprocess.rb +1 -0
- data/lib/good_job/supervisor.rb +22 -15
- data/lib/good_job/systemd_service.rb +2 -0
- data/lib/good_job/thread_status.rb +1 -0
- data/lib/good_job/version.rb +2 -1
- data/lib/good_job.rb +60 -23
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6973f64ae5640b5157d72d26438c7bb155072f07a98829461324b548f0092a19
|
|
4
|
+
data.tar.gz: 5b9dbd764aa128a9b54797e1f0abe0912942a391d656070ca86f4302041fed33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
@@ -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
|
-
|
|
285
|
-
|
|
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
|
-
(
|
|
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
|
|
@@ -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
|
|
23
|
-
|
|
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
|
|
@@ -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",
|
|
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
|
|
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
|
|
@@ -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 =
|
|
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
|
|
@@ -15,7 +16,7 @@ module GoodJob
|
|
|
15
16
|
def records
|
|
16
17
|
@_records ||= begin
|
|
17
18
|
after_id = params[:after_id]
|
|
18
|
-
after_at =
|
|
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
|
|
@@ -92,7 +93,9 @@ module GoodJob
|
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
def filter_by_search_query(query)
|
|
95
|
-
|
|
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: 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
|
-
|
|
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"
|
|
@@ -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
|