good_job 4.22.1 → 4.23.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 +46 -0
- data/README.md +13 -1
- data/app/controllers/good_job/executions_controller.rb +11 -0
- data/app/models/concerns/good_job/filtered_arguments.rb +27 -0
- data/app/models/good_job/concurrency_claim.rb +2 -0
- data/app/models/good_job/cron_entry.rb +9 -11
- data/app/models/good_job/execution.rb +2 -0
- data/app/models/good_job/job/lockable.rb +67 -42
- data/app/models/good_job/job.rb +2 -0
- 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 +6 -6
- data/config/locales/pt-BR.yml +25 -23
- data/config/routes.rb +3 -1
- 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/good_job/adapter.rb +27 -27
- data/lib/good_job/capsule.rb +8 -8
- data/lib/good_job/capsule_tracker.rb +29 -11
- data/lib/good_job/cron_manager.rb +5 -6
- data/lib/good_job/instance_registry.rb +45 -0
- data/lib/good_job/job_performer.rb +2 -1
- data/lib/good_job/notifier.rb +5 -6
- data/lib/good_job/poller.rb +4 -6
- data/lib/good_job/probe_server/simple_handler.rb +3 -0
- data/lib/good_job/probe_server/webrick_handler.rb +2 -0
- data/lib/good_job/probe_server.rb +2 -0
- data/lib/good_job/ractors.rb +2 -0
- data/lib/good_job/scheduler.rb +3 -6
- data/lib/good_job/shared_executor.rb +4 -6
- data/lib/good_job/systemd_service.rb +1 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +13 -2
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62018fa8fb4313f6aa3e6ef7531ac3073adc80449fd28a03f655014ce0c1922b
|
|
4
|
+
data.tar.gz: 478315a41ea1d69a9d15781b4b57ecd099504c3da754605b12fd752a10cb3ad0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f3f11b6633b90116e2cbe15057032cdd49609c074a80c21ef496ec250dea79f9ce5f3b3eda27db5c36bfd4cd1c262fda7b41301a168772148d70d35ca3f7dc7
|
|
7
|
+
data.tar.gz: 33ef016c46794b39cd7006b16073849c76d62116ab2c42030087b91286ebfd20887bf8735c7f0183fcd8a5acb4c599ae7eabbddcb36420f3a2484044301fc231
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v4.23.1](https://github.com/bensheldon/good_job/tree/v4.23.1) (2026-10-27)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.0...v4.23.1)
|
|
6
|
+
|
|
7
|
+
**Fixed bugs:**
|
|
8
|
+
|
|
9
|
+
- Release abandoned job claims after database failures [\#1887](https://github.com/bensheldon/good_job/pull/1887) ([ollym](https://github.com/ollym))
|
|
10
|
+
|
|
11
|
+
## [v4.23.0](https://github.com/bensheldon/good_job/tree/v4.23.0) (2026-10-27)
|
|
12
|
+
|
|
13
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.1...v4.23.0)
|
|
14
|
+
|
|
15
|
+
**Implemented enhancements:**
|
|
16
|
+
|
|
17
|
+
- Filter sensitive job arguments in the Dashboard [\#1881](https://github.com/bensheldon/good_job/pull/1881) ([bogdan](https://github.com/bogdan))
|
|
18
|
+
|
|
19
|
+
**Fixed bugs:**
|
|
20
|
+
|
|
21
|
+
- Respect enable\_listen\_notify when promoting concurrency claims [\#1892](https://github.com/bensheldon/good_job/pull/1892) ([eidam](https://github.com/eidam))
|
|
22
|
+
- Downgrade lock\_type before releasing advisory lock [\#1883](https://github.com/bensheldon/good_job/pull/1883) ([jmsanders](https://github.com/jmsanders))
|
|
23
|
+
|
|
24
|
+
**Closed issues:**
|
|
25
|
+
|
|
26
|
+
- ConcurrencyClaim sends NOTIFY even when enable\_listen\_notify is false [\#1891](https://github.com/bensheldon/good_job/issues/1891)
|
|
27
|
+
|
|
28
|
+
**Merged pull requests:**
|
|
29
|
+
|
|
30
|
+
- Keep JRuby platform in Gemfile.lock when Bundler re-resolves [\#1893](https://github.com/bensheldon/good_job/pull/1893) ([bensheldon](https://github.com/bensheldon))
|
|
31
|
+
- 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))
|
|
32
|
+
- Pin CI test runners to ubuntu-24.04 [\#1888](https://github.com/bensheldon/good_job/pull/1888) ([bensheldon](https://github.com/bensheldon))
|
|
33
|
+
- Require the concurrent-ruby classes GoodJob uses [\#1885](https://github.com/bensheldon/good_job/pull/1885) ([czj](https://github.com/czj))
|
|
34
|
+
- Add index\_good\_job\_executions\_on\_created\_at [\#1884](https://github.com/bensheldon/good_job/pull/1884) ([clinejj](https://github.com/clinejj))
|
|
35
|
+
- Demo: cap YJIT memory and shrink the Rack::Attack store [\#1880](https://github.com/bensheldon/good_job/pull/1880) ([bensheldon](https://github.com/bensheldon))
|
|
36
|
+
- 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))
|
|
37
|
+
- 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))
|
|
38
|
+
- Return 404 for GET /jobs/mass\_update instead of redirecting [\#1877](https://github.com/bensheldon/good_job/pull/1877) ([bensheldon](https://github.com/bensheldon))
|
|
39
|
+
- Load execution full traces and serialized params on demand [\#1876](https://github.com/bensheldon/good_job/pull/1876) ([bensheldon](https://github.com/bensheldon))
|
|
40
|
+
- Add specs for the demo's Rack::Attack rules [\#1875](https://github.com/bensheldon/good_job/pull/1875) ([bensheldon](https://github.com/bensheldon))
|
|
41
|
+
- 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))
|
|
42
|
+
- Reduce demo app memory and crawler load [\#1873](https://github.com/bensheldon/good_job/pull/1873) ([bensheldon](https://github.com/bensheldon))
|
|
43
|
+
- Fix JRuby flake from inspect in wait\_until predicate expectations [\#1872](https://github.com/bensheldon/good_job/pull/1872) ([bensheldon](https://github.com/bensheldon))
|
|
44
|
+
- Remove Adapter::RUNTIMES so adapters can be garbage collected [\#1870](https://github.com/bensheldon/good_job/pull/1870) ([bensheldon](https://github.com/bensheldon))
|
|
45
|
+
- 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))
|
|
46
|
+
- Use resolve\_\[property\] naming for CronEntry configuration resolvers [\#1868](https://github.com/bensheldon/good_job/pull/1868) ([bensheldon](https://github.com/bensheldon))
|
|
47
|
+
- Fix inconsistencies in the Brazilian Portuguese locale [\#1867](https://github.com/bensheldon/good_job/pull/1867) ([tiagoamaro](https://github.com/tiagoamaro))
|
|
48
|
+
|
|
3
49
|
## [v4.22.1](https://github.com/bensheldon/good_job/tree/v4.22.1) (2026-10-13)
|
|
4
50
|
|
|
5
51
|
[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)
|
|
@@ -479,6 +480,15 @@ end
|
|
|
479
480
|
|
|
480
481
|
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
482
|
|
|
483
|
+
#### Filtering sensitive arguments
|
|
484
|
+
|
|
485
|
+
The Dashboard hides job arguments the same way Rails hides them from logs:
|
|
486
|
+
|
|
487
|
+
- Hash values whose keys match `Rails.application.config.filter_parameters` (e.g. `password:`) are displayed as `[FILTERED]`.
|
|
488
|
+
- 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`.
|
|
489
|
+
|
|
490
|
+
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.
|
|
491
|
+
|
|
482
492
|
#### Extending dashboard views
|
|
483
493
|
|
|
484
494
|
GoodJob exposes some views that are intended to be overridden by placing views in your application:
|
|
@@ -1215,6 +1225,8 @@ Note that when running jobs in `:inline` execution mode, `GoodJob.current_thread
|
|
|
1215
1225
|
|
|
1216
1226
|
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
1227
|
|
|
1228
|
+
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.
|
|
1229
|
+
|
|
1218
1230
|
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
1231
|
|
|
1220
1232
|
```ruby
|
|
@@ -1644,7 +1656,7 @@ end
|
|
|
1644
1656
|
GOOD_JOB_LOCK_STRATEGY=skiplocked
|
|
1645
1657
|
```
|
|
1646
1658
|
|
|
1647
|
-
|
|
1659
|
+
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
1660
|
|
|
1649
1661
|
#### PgBouncer configuration
|
|
1650
1662
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob
|
|
4
|
+
# Renders an execution's heavier details on demand so they aren't included in every job page.
|
|
5
|
+
class ExecutionsController < GoodJob::ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@execution = Execution.find_by!(id: params[:id], active_job_id: params[:job_id])
|
|
8
|
+
render layout: false
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob
|
|
4
|
+
# Hides sensitive Active Job arguments when displaying Job/Execution records in the Dashboard.
|
|
5
|
+
module FilteredArguments
|
|
6
|
+
# Active Job arguments with sensitive values replaced by +[FILTERED]+.
|
|
7
|
+
# Every argument is filtered when the job class has +log_arguments+ disabled
|
|
8
|
+
# (e.g. +config.active_job.log_arguments = false+); otherwise hash values whose
|
|
9
|
+
# keys match +Rails.application.config.filter_parameters+ are filtered.
|
|
10
|
+
# @return [Array]
|
|
11
|
+
def display_arguments
|
|
12
|
+
arguments = serialized_params.fetch("arguments", [])
|
|
13
|
+
return arguments.map { ActiveSupport::ParameterFilter::FILTERED } unless log_arguments?
|
|
14
|
+
|
|
15
|
+
parameter_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
|
16
|
+
parameter_filter.filter("arguments" => arguments)["arguments"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def log_arguments?
|
|
22
|
+
active_job_class = job_class&.safe_constantize
|
|
23
|
+
active_job_class = ActiveJob::Base unless active_job_class.respond_to?(:log_arguments?)
|
|
24
|
+
active_job_class.log_arguments?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -172,6 +172,8 @@ module GoodJob
|
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
def notify(job_id)
|
|
175
|
+
return unless GoodJob.configuration.enable_listen_notify
|
|
176
|
+
|
|
175
177
|
queue_name = GoodJob::Job.where(id: job_id).pick(:queue_name)
|
|
176
178
|
GoodJob::Notifier.notify({ queue_name: queue_name }) if queue_name
|
|
177
179
|
end
|
|
@@ -139,8 +139,10 @@ module GoodJob # :nodoc:
|
|
|
139
139
|
job_klass = resolve_job_class
|
|
140
140
|
next unless job_klass.is_a?(Class)
|
|
141
141
|
|
|
142
|
-
configured_job = job_klass.set(
|
|
143
|
-
|
|
142
|
+
configured_job = job_klass.set(resolve_set)
|
|
143
|
+
job_args = resolve_args
|
|
144
|
+
job_kwargs = resolve_kwargs
|
|
145
|
+
job_kwargs.present? ? configured_job.perform_later(*job_args, **job_kwargs) : configured_job.perform_later(*job_args)
|
|
144
146
|
end
|
|
145
147
|
end
|
|
146
148
|
end
|
|
@@ -213,7 +215,8 @@ module GoodJob # :nodoc:
|
|
|
213
215
|
end
|
|
214
216
|
|
|
215
217
|
def resolve_job_class
|
|
216
|
-
value =
|
|
218
|
+
value = job_class
|
|
219
|
+
value = value.call if value.respond_to?(:call)
|
|
217
220
|
value.is_a?(String) ? value.constantize : value
|
|
218
221
|
end
|
|
219
222
|
|
|
@@ -236,22 +239,17 @@ module GoodJob # :nodoc:
|
|
|
236
239
|
schedule if schedule.instance_of?(Fugit::Cron)
|
|
237
240
|
end
|
|
238
241
|
|
|
239
|
-
def
|
|
240
|
-
value = job_class || nil
|
|
241
|
-
value.respond_to?(:call) ? value.call : value
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def set_value
|
|
242
|
+
def resolve_set
|
|
245
243
|
value = set || {}
|
|
246
244
|
value.respond_to?(:call) ? value.call : value
|
|
247
245
|
end
|
|
248
246
|
|
|
249
|
-
def
|
|
247
|
+
def resolve_args
|
|
250
248
|
value = args || []
|
|
251
249
|
value.respond_to?(:call) ? value.call : value
|
|
252
250
|
end
|
|
253
251
|
|
|
254
|
-
def
|
|
252
|
+
def resolve_kwargs
|
|
255
253
|
value = kwargs || nil
|
|
256
254
|
value.respond_to?(:call) ? value.call : value
|
|
257
255
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module GoodJob # :nodoc:
|
|
4
4
|
class Execution < BaseRecord
|
|
5
5
|
include ErrorEvents
|
|
6
|
+
include FilteredArguments
|
|
6
7
|
|
|
7
8
|
self.table_name = 'good_job_executions'
|
|
8
9
|
self.implicit_order_column = 'created_at'
|
|
@@ -63,6 +64,7 @@ module GoodJob # :nodoc:
|
|
|
63
64
|
|
|
64
65
|
def display_serialized_params
|
|
65
66
|
serialized_params.merge({
|
|
67
|
+
"arguments" => display_arguments,
|
|
66
68
|
_good_job_execution: attributes.except('serialized_params'),
|
|
67
69
|
})
|
|
68
70
|
end
|
|
@@ -8,6 +8,11 @@ module GoodJob
|
|
|
8
8
|
module Lockable
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
|
+
# Seconds to wait between attempts to release a claim after a connection failure.
|
|
12
|
+
RELEASE_CLAIM_RETRY_INTERVAL = 5
|
|
13
|
+
# Log a warning on the first failed release and then every this many attempts.
|
|
14
|
+
RELEASE_CLAIM_RETRY_WARNING_EVERY = 10
|
|
15
|
+
|
|
11
16
|
class_methods do
|
|
12
17
|
# Claims a job by acquiring a session-level advisory lock via the CTE
|
|
13
18
|
# query, and records the lock acquisition for phantom re-entrancy cleanup.
|
|
@@ -20,13 +25,17 @@ module GoodJob
|
|
|
20
25
|
def with_advisory_lock_claim(select_limit: nil, &block)
|
|
21
26
|
raise ArgumentError, "Must provide a block" unless block
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
record = nil
|
|
29
|
+
begin
|
|
30
|
+
advisory_lock(select_limit: select_limit) do |records|
|
|
31
|
+
record = records.first
|
|
26
32
|
yield(record)
|
|
27
|
-
ensure
|
|
28
|
-
record&.run_callbacks(:perform_unlocked)
|
|
29
33
|
end
|
|
34
|
+
ensure
|
|
35
|
+
# Runs after the advisory lock is released. A failed completion leaves the
|
|
36
|
+
# row lock in the database and may leave unsaved completion fields on the
|
|
37
|
+
# record, which batch callbacks would act on.
|
|
38
|
+
record.run_callbacks(:perform_unlocked) if record && (record.destroyed? || record.locked_by_id_in_database.nil?)
|
|
30
39
|
end
|
|
31
40
|
end
|
|
32
41
|
|
|
@@ -40,30 +49,7 @@ module GoodJob
|
|
|
40
49
|
# @yield [GoodJob::Job, nil] The claimed record, or nil if none found
|
|
41
50
|
# @return [Object] The return value of the block
|
|
42
51
|
def with_skip_locked_claim(locked_by_id:, locked_at:, lock_type:, &block)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
candidate_sql = select(:id).lock("FOR NO KEY UPDATE SKIP LOCKED").to_sql
|
|
46
|
-
materialized = supports_cte_materialization_specifiers? ? "MATERIALIZED " : ""
|
|
47
|
-
|
|
48
|
-
sql = <<~SQL.squish
|
|
49
|
-
WITH candidate AS #{materialized}(#{candidate_sql})
|
|
50
|
-
UPDATE #{quoted_table_name}
|
|
51
|
-
SET locked_by_id = ?,
|
|
52
|
-
locked_at = ?,
|
|
53
|
-
lock_type = ?
|
|
54
|
-
FROM candidate
|
|
55
|
-
WHERE #{quoted_table_name}.id = candidate.id
|
|
56
|
-
RETURNING #{quoted_table_name}.*
|
|
57
|
-
SQL
|
|
58
|
-
|
|
59
|
-
record = unscoped.find_by_sql([sql, locked_by_id, locked_at, lock_types[lock_type.to_s]]).first
|
|
60
|
-
|
|
61
|
-
begin
|
|
62
|
-
unscoped { yield(record) }
|
|
63
|
-
ensure
|
|
64
|
-
record.update(locked_by_id: nil, locked_at: nil, lock_type: nil) if record && !record.destroyed? && record.locked_by_id.present?
|
|
65
|
-
record&.run_callbacks(:perform_unlocked)
|
|
66
|
-
end
|
|
52
|
+
with_row_lock_claim(locked_by_id: locked_by_id, locked_at: locked_at, lock_type: lock_type, &block)
|
|
67
53
|
end
|
|
68
54
|
|
|
69
55
|
# Atomically claims a job using SELECT FOR NO KEY UPDATE SKIP LOCKED with an
|
|
@@ -77,13 +63,21 @@ module GoodJob
|
|
|
77
63
|
# @yield [GoodJob::Job, nil] The claimed record, or nil if none found
|
|
78
64
|
# @return [Object] The return value of the block
|
|
79
65
|
def with_hybrid_lock_claim(locked_by_id:, locked_at:, lock_type:, &block)
|
|
80
|
-
|
|
66
|
+
with_row_lock_claim(locked_by_id: locked_by_id, locked_at: locked_at, lock_type: lock_type, advisory: true, &block)
|
|
67
|
+
end
|
|
81
68
|
|
|
82
|
-
|
|
69
|
+
private
|
|
83
70
|
|
|
71
|
+
def with_row_lock_claim(locked_by_id:, locked_at:, lock_type:, advisory: false, &block)
|
|
72
|
+
raise ArgumentError, "Must provide a block" unless block
|
|
73
|
+
|
|
74
|
+
connection = lease_connection if advisory # retain the session holding the advisory lock
|
|
84
75
|
candidate_sql = select(:id).lock("FOR NO KEY UPDATE SKIP LOCKED").to_sql
|
|
85
|
-
advisory_lock_expr = "('x' || substr(md5(#{_quoted_table_name_string} || '-' || id::text), 1, 16))::bit(64)::bigint"
|
|
86
76
|
materialized = supports_cte_materialization_specifiers? ? "MATERIALIZED " : ""
|
|
77
|
+
candidate_filter = if advisory
|
|
78
|
+
advisory_key = advisory_lock_bigint_sql("#{_quoted_table_name_string} || '-' || id::text")
|
|
79
|
+
"WHERE pg_try_advisory_lock(#{advisory_key})"
|
|
80
|
+
end
|
|
87
81
|
|
|
88
82
|
sql = <<~SQL.squish
|
|
89
83
|
WITH candidate AS #{materialized}(#{candidate_sql})
|
|
@@ -91,26 +85,57 @@ module GoodJob
|
|
|
91
85
|
SET locked_by_id = ?,
|
|
92
86
|
locked_at = ?,
|
|
93
87
|
lock_type = ?
|
|
94
|
-
FROM (
|
|
95
|
-
SELECT id FROM candidate
|
|
96
|
-
WHERE pg_try_advisory_lock(#{advisory_lock_expr})
|
|
97
|
-
) AS locked
|
|
88
|
+
FROM (SELECT id FROM candidate #{candidate_filter}) AS locked
|
|
98
89
|
WHERE #{quoted_table_name}.id = locked.id
|
|
99
90
|
RETURNING #{quoted_table_name}.*
|
|
100
91
|
SQL
|
|
101
92
|
|
|
102
93
|
record = unscoped.find_by_sql([sql, locked_by_id, locked_at, lock_types[lock_type.to_s]]).first
|
|
103
|
-
record_advisory_lock(
|
|
94
|
+
record_advisory_lock(connection, record.lockable_column_key, cte: true) if advisory && record
|
|
104
95
|
|
|
96
|
+
completed = false
|
|
105
97
|
begin
|
|
106
|
-
unscoped { yield(record) }
|
|
98
|
+
result = unscoped { yield(record) }
|
|
99
|
+
completed = true
|
|
100
|
+
result
|
|
107
101
|
ensure
|
|
108
|
-
|
|
109
|
-
record.advisory_unlock
|
|
110
|
-
|
|
102
|
+
begin
|
|
103
|
+
record.advisory_unlock(connection: connection) if advisory && record
|
|
104
|
+
ensure
|
|
105
|
+
# A failed completion may have cleared the attributes in memory only.
|
|
106
|
+
if record && (!completed || (!record.destroyed? && record.locked_by_id_in_database.present?))
|
|
107
|
+
released = release_lock_claim(record, locked_by_id: locked_by_id, locked_at: locked_at)
|
|
108
|
+
completed &&= released
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
record&.run_callbacks(:perform_unlocked) if completed
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def release_lock_claim(record, locked_by_id:, locked_at:)
|
|
116
|
+
transaction do
|
|
117
|
+
# Match the original claim, including its time, so a newer execution is untouched.
|
|
118
|
+
released = unscoped.where(id: record.id, locked_by_id: locked_by_id, locked_at: locked_at)
|
|
119
|
+
.update_all(locked_by_id: nil, locked_at: nil, lock_type: nil) # rubocop:disable Rails/SkipsModelValidations
|
|
120
|
+
if released.positive?
|
|
121
|
+
GoodJob::ConcurrencyClaim.release_job(record.id) if GoodJob::ConcurrencyClaim.table_exists?
|
|
122
|
+
record.reload
|
|
111
123
|
end
|
|
112
|
-
|
|
124
|
+
released.positive?
|
|
113
125
|
end
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
connection_errors = %w[ActiveRecord::ConnectionNotEstablished ActiveRecord::ConnectionFailed PG::ConnectionBad PG::UnableToSend].filter_map(&:safe_constantize)
|
|
128
|
+
cause = e.is_a?(ActiveRecord::StatementInvalid) ? e.cause : nil # older Rails wraps PG connection errors
|
|
129
|
+
raise unless connection_errors.any? { |klass| e.is_a?(klass) || cause.is_a?(klass) }
|
|
130
|
+
# Once the process is exiting, its stale process record lets cleanup release the claim.
|
|
131
|
+
return false if Thread.current.status == "aborting" || GoodJob.current_thread_shutting_down?
|
|
132
|
+
|
|
133
|
+
# Keep the worker registered until its claim can be released. Otherwise a
|
|
134
|
+
# live process can keep the abandoned job locked indefinitely.
|
|
135
|
+
attempts = (attempts || 0) + 1
|
|
136
|
+
GoodJob.logger.warn("GoodJob could not release the claim on job #{record.id} (attempt #{attempts}): #{e.class}: #{e.message}. Retrying every #{RELEASE_CLAIM_RETRY_INTERVAL} seconds.") if attempts == 1 || (attempts % RELEASE_CLAIM_RETRY_WARNING_EVERY).zero?
|
|
137
|
+
sleep RELEASE_CLAIM_RETRY_INTERVAL
|
|
138
|
+
retry
|
|
114
139
|
end
|
|
115
140
|
end
|
|
116
141
|
end
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -6,6 +6,7 @@ module GoodJob
|
|
|
6
6
|
include AdvisoryLockable
|
|
7
7
|
include Job::Lockable
|
|
8
8
|
include ErrorEvents
|
|
9
|
+
include FilteredArguments
|
|
9
10
|
include Filterable
|
|
10
11
|
include Reportable
|
|
11
12
|
|
|
@@ -622,6 +623,7 @@ module GoodJob
|
|
|
622
623
|
# @return [Hash]
|
|
623
624
|
def display_serialized_params
|
|
624
625
|
serialized_params.merge({
|
|
626
|
+
"arguments" => display_arguments,
|
|
625
627
|
_good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
|
|
626
628
|
})
|
|
627
629
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%= content_tag "turbo-frame", id: dom_id(@execution, :full_trace) do %>
|
|
2
|
+
<div class="small">
|
|
3
|
+
<code class="text-wrap text-break m-0 text-secondary-emphasis">
|
|
4
|
+
<%= safe_join(@execution.error_backtrace || [], tag.br) %>
|
|
5
|
+
</code>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<%= content_tag "turbo-frame", id: dom_id(@execution, :serialized_params) do %>
|
|
10
|
+
<%= tag.pre JSON.pretty_generate(truncate_display_value(@execution.display_serialized_params)) %>
|
|
11
|
+
<% end %>
|
|
@@ -58,11 +58,7 @@
|
|
|
58
58
|
</div>
|
|
59
59
|
<% end %>
|
|
60
60
|
<%= tag.div class: "tab-pane fade", id: dom_id(execution, :full_pane), role: "tabpane", aria: { labelledby: dom_id(execution, :full) }, tabindex: 0 do %>
|
|
61
|
-
|
|
62
|
-
<code class="text-wrap text-break m-0 text-secondary-emphasis">
|
|
63
|
-
<%= safe_join(execution.error_backtrace, tag.br) %>
|
|
64
|
-
</code>
|
|
65
|
-
</div>
|
|
61
|
+
<%= content_tag "turbo-frame", "", id: dom_id(execution, :full_trace), src: job_execution_path(@job.id, execution.id), loading: "lazy" %>
|
|
66
62
|
<% end %>
|
|
67
63
|
<% end %>
|
|
68
64
|
<% end %>
|
|
@@ -70,7 +66,7 @@
|
|
|
70
66
|
<% end %>
|
|
71
67
|
<%= render 'good_job/custom_execution_details', execution: execution, job: @job %>
|
|
72
68
|
<%= tag.div id: dom_id(execution, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
|
|
73
|
-
<%=
|
|
69
|
+
<%= content_tag "turbo-frame", "", id: dom_id(execution, :serialized_params), src: job_execution_path(@job.id, execution.id), loading: "lazy" %>
|
|
74
70
|
<% end %>
|
|
75
71
|
<% end %>
|
|
76
72
|
</div>
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
<% end %>
|
|
86
86
|
</h5>
|
|
87
87
|
</div>
|
|
88
|
-
<%= tag.pre @job.
|
|
88
|
+
<%= tag.pre @job.display_arguments.map { |argument| truncate_display_value(argument).inspect }.join(', '), class: 'text-wrap text-break' %>
|
|
89
89
|
|
|
90
90
|
<%= tag.div id: dom_id(@job, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
|
|
91
91
|
<%= tag.pre JSON.pretty_generate(truncate_display_value(@job.display_serialized_params)) %>
|
data/config/brakeman.ignore
CHANGED
|
@@ -119,25 +119,25 @@
|
|
|
119
119
|
{
|
|
120
120
|
"warning_type": "SQL Injection",
|
|
121
121
|
"warning_code": 0,
|
|
122
|
-
"fingerprint": "
|
|
122
|
+
"fingerprint": "3f2f0feed3442f5946899d5884a014ecb86894269ac9685db90eeeb6f3a06f48",
|
|
123
123
|
"check_name": "SQL",
|
|
124
124
|
"message": "Possible SQL injection",
|
|
125
125
|
"file": "app/models/good_job/job/lockable.rb",
|
|
126
|
-
"line":
|
|
126
|
+
"line": 80,
|
|
127
127
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
128
|
-
"code": "unscoped.find_by_sql([\"WITH candidate AS #{(\"MATERIALIZED \" or \"\")}(#{select(:id).lock(\"FOR NO KEY UPDATE SKIP LOCKED\").to_sql})\\nUPDATE #{quoted_table_name}\\nSET locked_by_id = ?,\\n locked_at = ?,\\n lock_type = ?\\nFROM (
|
|
128
|
+
"code": "unscoped.find_by_sql([\"WITH candidate AS #{(\"MATERIALIZED \" or \"\")}(#{select(:id).lock(\"FOR NO KEY UPDATE SKIP LOCKED\").to_sql})\\nUPDATE #{quoted_table_name}\\nSET locked_by_id = ?,\\n locked_at = ?,\\n lock_type = ?\\nFROM (SELECT id FROM candidate #{if advisory then\n advisory_key = advisory_lock_bigint_sql(\"#{_quoted_table_name_string} || '-' || id::text\")\n \"WHERE pg_try_advisory_lock(#{advisory_lock_bigint_sql(\"#{_quoted_table_name_string} || '-' || id::text\")})\"\nend}) AS locked\\nWHERE #{quoted_table_name}.id = locked.id\\nRETURNING #{quoted_table_name}.*\\n\".squish, locked_by_id, locked_at, lock_types[lock_type.to_s]])",
|
|
129
129
|
"render_path": null,
|
|
130
130
|
"location": {
|
|
131
131
|
"type": "method",
|
|
132
132
|
"class": "GoodJob::Job",
|
|
133
|
-
"method": "
|
|
133
|
+
"method": "with_row_lock_claim"
|
|
134
134
|
},
|
|
135
|
-
"user_input": "_quoted_table_name_string",
|
|
135
|
+
"user_input": "if advisory then\n advisory_key = advisory_lock_bigint_sql(\"#{_quoted_table_name_string} || '-' || id::text\")\n \"WHERE pg_try_advisory_lock(#{advisory_lock_bigint_sql(\"#{_quoted_table_name_string} || '-' || id::text\")})\"\nend",
|
|
136
136
|
"confidence": "Weak",
|
|
137
137
|
"cwe_id": [
|
|
138
138
|
89
|
|
139
139
|
],
|
|
140
|
-
"note": "Table
|
|
140
|
+
"note": "Claim query refactored into with_row_lock_claim. Table names are Rails-quoted; candidate SQL is Rails-generated; the optional advisory predicate uses only the configured hash and an escaped table name. All varying claim values are bound. No untrusted SQL input."
|
|
141
141
|
}
|
|
142
142
|
],
|
|
143
143
|
"brakeman_version": "8.0.4"
|
data/config/locales/pt-BR.yml
CHANGED
|
@@ -37,17 +37,17 @@ pt-BR:
|
|
|
37
37
|
no_batches_found: Nenhum lote encontrado.
|
|
38
38
|
cleaner:
|
|
39
39
|
index:
|
|
40
|
-
all:
|
|
41
|
-
class:
|
|
42
|
-
exception:
|
|
43
|
-
grouped_by_class:
|
|
44
|
-
grouped_by_exception:
|
|
45
|
-
last_1_hour:
|
|
46
|
-
last_24_hours:
|
|
47
|
-
last_3_days:
|
|
48
|
-
last_3_hours:
|
|
49
|
-
last_7_days:
|
|
50
|
-
title:
|
|
40
|
+
all: Tudo
|
|
41
|
+
class: Classe
|
|
42
|
+
exception: Exceção
|
|
43
|
+
grouped_by_class: Descartes agrupados por Classe
|
|
44
|
+
grouped_by_exception: Descartes agrupados por Exceção
|
|
45
|
+
last_1_hour: Última hora
|
|
46
|
+
last_24_hours: Últimas 24 horas
|
|
47
|
+
last_3_days: Últimos 3 dias
|
|
48
|
+
last_3_hours: Últimas 3 horas
|
|
49
|
+
last_7_days: Últimos 7 dias
|
|
50
|
+
title: Limpeza de Descartes
|
|
51
51
|
total: Total
|
|
52
52
|
concurrency_claims:
|
|
53
53
|
states:
|
|
@@ -142,12 +142,12 @@ pt-BR:
|
|
|
142
142
|
reschedule: Reagendar tarefa
|
|
143
143
|
retry: Tentar tarefa novamente
|
|
144
144
|
destroy:
|
|
145
|
-
notice: A tarefa foi
|
|
145
|
+
notice: A tarefa foi excluída.
|
|
146
146
|
discard:
|
|
147
147
|
notice: A tarefa foi descartada.
|
|
148
148
|
executions:
|
|
149
|
-
application_trace:
|
|
150
|
-
full_trace:
|
|
149
|
+
application_trace: Trace da aplicação
|
|
150
|
+
full_trace: Trace completo
|
|
151
151
|
in_queue: na fila
|
|
152
152
|
recovery: recuperação
|
|
153
153
|
runtime: tempo de execução
|
|
@@ -175,7 +175,7 @@ pt-BR:
|
|
|
175
175
|
destroy_all: Excluir tudo
|
|
176
176
|
discard_all: Descartar tudo
|
|
177
177
|
reschedule_all: Reagendar tudo
|
|
178
|
-
retry_all: Tentar novamente
|
|
178
|
+
retry_all: Tentar tudo novamente
|
|
179
179
|
title: Ações
|
|
180
180
|
no_jobs_found: Nenhuma tarefa encontrada.
|
|
181
181
|
toggle_actions: Alternar Ações
|
|
@@ -204,7 +204,7 @@ pt-BR:
|
|
|
204
204
|
job:
|
|
205
205
|
arguments: Argumentos
|
|
206
206
|
attempts: Tentativas
|
|
207
|
-
concurrency_claims:
|
|
207
|
+
concurrency_claims: Reservas de concorrência
|
|
208
208
|
labels: Rótulos
|
|
209
209
|
priority: Prioridade
|
|
210
210
|
queue: Fila
|
|
@@ -230,7 +230,7 @@ pt-BR:
|
|
|
230
230
|
confirm_pause: Tem certeza que deseja pausar %{value}?
|
|
231
231
|
confirm_unpause: Tem certeza que deseja retomar %{value}?
|
|
232
232
|
disabled: O recurso experimental de Pausas do GoodJob está desativado por padrão porque pode degradar o desempenho. Configure para ativar
|
|
233
|
-
job_class: Classe
|
|
233
|
+
job_class: Classe da tarefa
|
|
234
234
|
label: Rótulo
|
|
235
235
|
pause: Pausar
|
|
236
236
|
queue: Fila
|
|
@@ -245,8 +245,8 @@ pt-BR:
|
|
|
245
245
|
chart_title_total: Latência total média em segundos
|
|
246
246
|
execution_latency: Latência de execução
|
|
247
247
|
executions: Execuções
|
|
248
|
-
job_class: Classe
|
|
249
|
-
jobs:
|
|
248
|
+
job_class: Classe da tarefa
|
|
249
|
+
jobs: Tarefas
|
|
250
250
|
no_executions: Nenhuma execução neste intervalo de tempo.
|
|
251
251
|
queue_latency: Latência na fila
|
|
252
252
|
queue_name: Nome da fila
|
|
@@ -267,7 +267,9 @@ pt-BR:
|
|
|
267
267
|
days: "%{count}d"
|
|
268
268
|
hours: "%{count}h"
|
|
269
269
|
minutes: "%{count}m"
|
|
270
|
-
months:
|
|
270
|
+
months:
|
|
271
|
+
one: "~%{count} mês"
|
|
272
|
+
other: "~%{count} meses"
|
|
271
273
|
seconds: "%{count}s"
|
|
272
274
|
years: "~%{count}a"
|
|
273
275
|
chart_bucket_size: 'Granularidade temporal do gráfico: %{size}'
|
|
@@ -317,8 +319,8 @@ pt-BR:
|
|
|
317
319
|
search: Pesquisar
|
|
318
320
|
navbar:
|
|
319
321
|
batches: Lotes
|
|
320
|
-
cleaner:
|
|
321
|
-
cron_schedules:
|
|
322
|
+
cleaner: Limpeza de Descartes
|
|
323
|
+
cron_schedules: Tarefas Programadas
|
|
322
324
|
jobs: Tarefas
|
|
323
325
|
live_poll: Acompanhamento ao Vivo
|
|
324
326
|
name: "GoodJob 👍"
|
|
@@ -332,7 +334,7 @@ pt-BR:
|
|
|
332
334
|
theme: Tema
|
|
333
335
|
pending_migrations: O GoodJob tem migrações pendentes no banco de dados.
|
|
334
336
|
secondary_navbar:
|
|
335
|
-
inspiration: Lembre-se, você também está fazendo
|
|
337
|
+
inspiration: Lembre-se, você também está fazendo um Bom Trabalho!
|
|
336
338
|
last_updated: Última atualização
|
|
337
339
|
status:
|
|
338
340
|
discarded: Descartado
|
data/config/routes.rb
CHANGED
|
@@ -5,7 +5,7 @@ GoodJob::Engine.routes.draw do
|
|
|
5
5
|
|
|
6
6
|
resources :jobs, only: %i[index show destroy] do
|
|
7
7
|
collection do
|
|
8
|
-
get :mass_update, to:
|
|
8
|
+
get :mass_update, to: ->(_env) { raise ActionController::RoutingError, 'Not Found' }
|
|
9
9
|
put :mass_update
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -15,6 +15,8 @@ GoodJob::Engine.routes.draw do
|
|
|
15
15
|
put :reschedule
|
|
16
16
|
put :retry
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
resources :executions, only: %i[show]
|
|
18
20
|
end
|
|
19
21
|
get 'jobs/metrics/primary_nav', to: 'metrics#primary_nav', as: :metrics_primary_nav
|
|
20
22
|
get 'jobs/metrics/job_status', to: 'metrics#job_status', as: :metrics_job_status
|
|
@@ -114,6 +114,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
114
114
|
|
|
115
115
|
add_index :good_job_executions, [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
|
|
116
116
|
add_index :good_job_executions, :scheduled_at, name: :index_good_job_executions_on_scheduled_at
|
|
117
|
+
add_index :good_job_executions, :created_at, name: :index_good_job_executions_on_created_at
|
|
117
118
|
add_index :good_jobs, [:priority, :scheduled_at, :id],
|
|
118
119
|
where: "finished_at IS NULL", name: "index_good_jobs_on_priority_scheduled_at_unfinished"
|
|
119
120
|
add_index :good_jobs, [:queue_name, :scheduled_at, :id],
|
|
@@ -97,6 +97,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
97
97
|
|
|
98
98
|
add_index :good_job_executions, [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
|
|
99
99
|
add_index :good_job_executions, :scheduled_at, name: :index_good_job_executions_on_scheduled_at
|
|
100
|
+
add_index :good_job_executions, :created_at, name: :index_good_job_executions_on_created_at
|
|
100
101
|
add_index :good_jobs, :locked_by_id,
|
|
101
102
|
where: "locked_by_id IS NOT NULL", name: "index_good_jobs_on_locked_by_id"
|
|
102
103
|
add_index :good_jobs, [:priority, :scheduled_at, :id],
|