good_job 4.23.0 → 4.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +7 -1
- 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 +1 -0
- 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 +1 -0
- 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 +1 -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 +1 -0
- 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 +1 -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 +1 -0
- data/app/models/good_job/cron_entry.rb +1 -0
- data/app/models/good_job/discrete_execution.rb +1 -0
- data/app/models/good_job/execution.rb +6 -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 +20 -16
- data/app/models/good_job/process.rb +5 -5
- data/app/models/good_job/setting.rb +5 -2
- data/config/brakeman.ignore +9 -9
- 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/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 +25 -32
- data/lib/good_job/bulk.rb +1 -0
- data/lib/good_job/callable.rb +1 -0
- data/lib/good_job/capsule.rb +4 -0
- data/lib/good_job/capsule_tracker.rb +6 -6
- 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 +1 -0
- 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 +5 -2
- 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 +5 -4
- 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 +1 -0
- data/lib/good_job/poller.rb +1 -0
- 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 +48 -11
- data/lib/good_job/probe_server/webrick_handler.rb +1 -0
- data/lib/good_job/probe_server.rb +1 -0
- data/lib/good_job/ractors.rb +1 -0
- data/lib/good_job/safe_state.rb +1 -0
- data/lib/good_job/scheduler.rb +1 -0
- data/lib/good_job/sd_notify.rb +1 -0
- data/lib/good_job/shared_executor.rb +1 -0
- data/lib/good_job/subprocess.rb +1 -0
- data/lib/good_job/supervisor.rb +22 -15
- data/lib/good_job/systemd_service.rb +1 -0
- data/lib/good_job/thread_status.rb +1 -0
- data/lib/good_job/version.rb +2 -1
- data/lib/good_job.rb +47 -21
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca3ea73eb3548ddaa1b84dab18b341c9c8182f91d52b82a41681cfa2080550da
|
|
4
|
+
data.tar.gz: 9fc5590d811d1b04c847054d685d9f911910356c33d0726d1ba1620a2228caf5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2bca41349e888ed1e16ad8a3da30cfb17390eb1451803aa14b2bed95c39494a6cf83b7eb29a6cb39d86b1545756d58ced8c3cbf4acd5d53d56873d2652ce5766
|
|
7
|
+
data.tar.gz: f10b1e0bd0f51edce6879785642730d96367fe2f4fdd5d4206576c510993950fb278db1e38d3ed6c3a2bd1a9050aa99502a4e81a2e8dd3f2a78806c5c509b0a0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v4.24.0](https://github.com/bensheldon/good_job/tree/v4.24.0) (2026-10-29)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.1...v4.24.0)
|
|
6
|
+
|
|
7
|
+
**Implemented enhancements:**
|
|
8
|
+
|
|
9
|
+
- Add a separate retention period for discarded jobs [\#1897](https://github.com/bensheldon/good_job/pull/1897) ([IslamElsayed](https://github.com/IslamElsayed))
|
|
10
|
+
|
|
11
|
+
**Fixed bugs:**
|
|
12
|
+
|
|
13
|
+
- 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))
|
|
14
|
+
- Validate advisory lock SQL identifiers and harden dashboard filter params [\#1901](https://github.com/bensheldon/good_job/pull/1901) ([bensheldon](https://github.com/bensheldon))
|
|
15
|
+
- 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))
|
|
16
|
+
|
|
17
|
+
**Closed issues:**
|
|
18
|
+
|
|
19
|
+
- \[feature request\] Cleanup older discarded jobs [\#1597](https://github.com/bensheldon/good_job/issues/1597)
|
|
20
|
+
|
|
21
|
+
**Merged pull requests:**
|
|
22
|
+
|
|
23
|
+
- 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))
|
|
24
|
+
- 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))
|
|
25
|
+
- Fix flaky JRuby FrozenError by preloading record schemas in specs [\#1896](https://github.com/bensheldon/good_job/pull/1896) ([bensheldon](https://github.com/bensheldon))
|
|
26
|
+
- Demo: shorten database connection lifetimes [\#1895](https://github.com/bensheldon/good_job/pull/1895) ([bensheldon](https://github.com/bensheldon))
|
|
27
|
+
|
|
28
|
+
## [v4.23.1](https://github.com/bensheldon/good_job/tree/v4.23.1) (2026-10-27)
|
|
29
|
+
|
|
30
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.0...v4.23.1)
|
|
31
|
+
|
|
32
|
+
**Fixed bugs:**
|
|
33
|
+
|
|
34
|
+
- Release abandoned job claims after database failures [\#1887](https://github.com/bensheldon/good_job/pull/1887) ([ollym](https://github.com/ollym))
|
|
35
|
+
|
|
3
36
|
## [v4.23.0](https://github.com/bensheldon/good_job/tree/v4.23.0) (2026-10-27)
|
|
4
37
|
|
|
5
38
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.1...v4.23.0)
|
data/README.md
CHANGED
|
@@ -236,7 +236,8 @@ Usage:
|
|
|
236
236
|
good_job cleanup_preserved_jobs
|
|
237
237
|
|
|
238
238
|
Options:
|
|
239
|
-
[--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)
|
|
240
241
|
|
|
241
242
|
Manually destroys preserved job records.
|
|
242
243
|
|
|
@@ -317,6 +318,7 @@ Available configuration options are:
|
|
|
317
318
|
- `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
|
|
318
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`.
|
|
319
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`.
|
|
320
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`).
|
|
321
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`).
|
|
322
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`).
|
|
@@ -1225,6 +1227,8 @@ Note that when running jobs in `:inline` execution mode, `GoodJob.current_thread
|
|
|
1225
1227
|
|
|
1226
1228
|
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.
|
|
1227
1229
|
|
|
1230
|
+
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.
|
|
1231
|
+
|
|
1228
1232
|
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.
|
|
1229
1233
|
|
|
1230
1234
|
```ruby
|
|
@@ -1598,6 +1602,7 @@ GoodJob will automatically delete preserved job records after 14 days. The reten
|
|
|
1598
1602
|
|
|
1599
1603
|
```ruby
|
|
1600
1604
|
config.good_job.cleanup_preserved_jobs_before_seconds_ago = 14.days
|
|
1605
|
+
config.good_job.cleanup_discarded_jobs_before_seconds_ago = 90.days # Optional: keep discarded jobs longer than the others.
|
|
1601
1606
|
config.good_job.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.
|
|
1602
1607
|
config.good_job.cleanup_interval_seconds = 10.minutes # Number of seconds between deletion sweeps.
|
|
1603
1608
|
```
|
|
@@ -1609,6 +1614,7 @@ It is also possible to manually trigger a cleanup of preserved job records:
|
|
|
1609
1614
|
```ruby
|
|
1610
1615
|
GoodJob.cleanup_preserved_jobs # Will use default retention period
|
|
1611
1616
|
GoodJob.cleanup_preserved_jobs(older_than: 7.days) # custom retention period
|
|
1617
|
+
GoodJob.cleanup_preserved_jobs(older_than: 7.days, discarded_older_than: 90.days) # keep discarded jobs longer
|
|
1612
1618
|
```
|
|
1613
1619
|
|
|
1614
1620
|
- For example, using the `good_job` command-line utility:
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: true
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module GoodJob
|
|
@@ -109,22 +110,22 @@ module GoodJob
|
|
|
109
110
|
|
|
110
111
|
begin
|
|
111
112
|
if build_result[:bulkable].any?
|
|
112
|
-
Job.transaction(requires_new: true, joinable: false) do
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
case lock_strategy
|
|
124
|
-
when :advisory, :hybrid
|
|
125
|
-
inline_jobs.each(&:advisory_lock!)
|
|
126
|
-
end
|
|
113
|
+
persisted_jobs, inline_jobs = Job.transaction(requires_new: true, joinable: false) do
|
|
114
|
+
inserted_jobs = _insert_jobs(build_result[:bulkable], build_result[:active_jobs_by_job_id])
|
|
115
|
+
next [inserted_jobs, []] unless execute_inline
|
|
116
|
+
|
|
117
|
+
jobs_to_inline = inserted_jobs.select { |job| job.scheduled_at.nil? || job.scheduled_at <= current_time }
|
|
118
|
+
if lock_strategy != :advisory && lock_id && jobs_to_inline.any?
|
|
119
|
+
Job.where(id: jobs_to_inline.map(&:id)).update_all( # rubocop:disable Rails/SkipsModelValidations
|
|
120
|
+
locked_by_id: lock_id, locked_at: current_time, lock_type: Job.lock_types[lock_strategy.to_s]
|
|
121
|
+
)
|
|
122
|
+
jobs_to_inline.each { |j| j.assign_attributes(locked_by_id: lock_id, locked_at: current_time, lock_type: lock_strategy) }
|
|
127
123
|
end
|
|
124
|
+
case lock_strategy
|
|
125
|
+
when :advisory, :hybrid
|
|
126
|
+
jobs_to_inline.each(&:advisory_lock!)
|
|
127
|
+
end
|
|
128
|
+
[inserted_jobs, jobs_to_inline]
|
|
128
129
|
end
|
|
129
130
|
end
|
|
130
131
|
|
|
@@ -444,7 +445,7 @@ module GoodJob
|
|
|
444
445
|
state = { queue_name: queue_name, count: grouped_jobs.size }
|
|
445
446
|
state[:scheduled_at] = scheduled_at if scheduled_at
|
|
446
447
|
|
|
447
|
-
executed_locally = adapter.respond_to?(:execute_async?) && adapter.execute_async? && GoodJob.capsule
|
|
448
|
+
executed_locally = adapter.respond_to?(:execute_async?) && adapter.execute_async? && GoodJob.capsule.create_thread(state)
|
|
448
449
|
unless executed_locally
|
|
449
450
|
state[:count] = grouped_jobs.count { |job| _send_notify?(active_jobs_by_job_id[job.active_job_id]) }
|
|
450
451
|
Notifier.notify(state) unless state[:count].zero?
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: true
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require 'active_job/arguments'
|
|
@@ -63,8 +64,8 @@ module GoodJob
|
|
|
63
64
|
if job_discarded && !discarded_at
|
|
64
65
|
update(discarded_at: Time.current)
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
discard_job_class = on_discard.presence&.constantize
|
|
68
|
+
if discard_job_class
|
|
68
69
|
Job.defer_after_commit_maybe(discard_job_class) do
|
|
69
70
|
Batch.within_thread(batch_id: nil, batch_callback_id: id) do
|
|
70
71
|
discard_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard })
|
|
@@ -76,8 +77,8 @@ module GoodJob
|
|
|
76
77
|
if enqueued_at && !(self.class.jobs_finished_at_migrated? ? jobs_finished_at : finished_at) && jobs.where(finished_at: nil).none?
|
|
77
78
|
self.class.jobs_finished_at_migrated? ? update(jobs_finished_at: Time.current) : update(finished_at: Time.current)
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
success_job_class = on_success.presence&.constantize unless discarded_at
|
|
81
|
+
if success_job_class
|
|
81
82
|
Job.defer_after_commit_maybe(success_job_class) do
|
|
82
83
|
Batch.within_thread(batch_id: nil, batch_callback_id: id) do
|
|
83
84
|
success_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success })
|
|
@@ -85,11 +86,11 @@ module GoodJob
|
|
|
85
86
|
end
|
|
86
87
|
end
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
finish_job_class = on_finish.presence&.constantize
|
|
90
|
+
if finish_job_class
|
|
90
91
|
Job.defer_after_commit_maybe(finish_job_class) do
|
|
91
92
|
Batch.within_thread(batch_id: nil, batch_callback_id: id) do
|
|
92
|
-
|
|
93
|
+
finish_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish })
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
end
|
|
@@ -113,7 +114,8 @@ module GoodJob
|
|
|
113
114
|
end
|
|
114
115
|
|
|
115
116
|
if Rails.gem_version < Gem::Version.new('7.1.0.alpha')
|
|
116
|
-
|
|
117
|
+
# Positional coder argument; Sorbet only knows the Rails 7.1+ signature
|
|
118
|
+
public_send(:serialize, :serialized_properties, PropertySerializer, default: -> { {} }) # rubocop:disable Style/SendWithLiteralMethodName
|
|
117
119
|
else
|
|
118
120
|
serialize :serialized_properties, coder: PropertySerializer, default: -> { {} }
|
|
119
121
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: true
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module GoodJob # :nodoc:
|
|
@@ -44,7 +45,7 @@ module GoodJob # :nodoc:
|
|
|
44
45
|
|
|
45
46
|
def status
|
|
46
47
|
if finished_at.present?
|
|
47
|
-
if error.present? && job
|
|
48
|
+
if error.present? && job&.finished_at.present?
|
|
48
49
|
:discarded
|
|
49
50
|
elsif error.present?
|
|
50
51
|
:retried
|
|
@@ -57,9 +58,11 @@ module GoodJob # :nodoc:
|
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def interrupted_duration
|
|
60
|
-
|
|
61
|
+
interrupted_at = finished_at
|
|
62
|
+
started_at = performed_at
|
|
63
|
+
return unless error_event == "interrupted" && interrupted_at && started_at
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
interrupted_at - started_at
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def display_serialized_params
|