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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +13 -1
  4. data/app/controllers/good_job/executions_controller.rb +11 -0
  5. data/app/models/concerns/good_job/filtered_arguments.rb +27 -0
  6. data/app/models/good_job/concurrency_claim.rb +2 -0
  7. data/app/models/good_job/cron_entry.rb +9 -11
  8. data/app/models/good_job/execution.rb +2 -0
  9. data/app/models/good_job/job/lockable.rb +67 -42
  10. data/app/models/good_job/job.rb +2 -0
  11. data/app/views/good_job/executions/show.html.erb +11 -0
  12. data/app/views/good_job/jobs/_executions.erb +2 -6
  13. data/app/views/good_job/jobs/show.html.erb +1 -1
  14. data/config/brakeman.ignore +6 -6
  15. data/config/locales/pt-BR.yml +25 -23
  16. data/config/routes.rb +3 -1
  17. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -0
  18. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +1 -0
  19. data/lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb +12 -0
  20. data/lib/good_job/adapter.rb +27 -27
  21. data/lib/good_job/capsule.rb +8 -8
  22. data/lib/good_job/capsule_tracker.rb +29 -11
  23. data/lib/good_job/cron_manager.rb +5 -6
  24. data/lib/good_job/instance_registry.rb +45 -0
  25. data/lib/good_job/job_performer.rb +2 -1
  26. data/lib/good_job/notifier.rb +5 -6
  27. data/lib/good_job/poller.rb +4 -6
  28. data/lib/good_job/probe_server/simple_handler.rb +3 -0
  29. data/lib/good_job/probe_server/webrick_handler.rb +2 -0
  30. data/lib/good_job/probe_server.rb +2 -0
  31. data/lib/good_job/ractors.rb +2 -0
  32. data/lib/good_job/scheduler.rb +3 -6
  33. data/lib/good_job/shared_executor.rb +4 -6
  34. data/lib/good_job/systemd_service.rb +1 -0
  35. data/lib/good_job/version.rb +1 -1
  36. data/lib/good_job.rb +13 -2
  37. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ce4bd5cfbb7e4fcc44f90099bc1aa892ef59e1294c69eac5d4279c3968db9c0
4
- data.tar.gz: '093ba2cc69d8d51f38bbf4cd93d9bf97aa65d995768f00a6b0d27dbe1aacdd08'
3
+ metadata.gz: 62018fa8fb4313f6aa3e6ef7531ac3073adc80449fd28a03f655014ce0c1922b
4
+ data.tar.gz: 478315a41ea1d69a9d15781b4b57ecd099504c3da754605b12fd752a10cb3ad0
5
5
  SHA512:
6
- metadata.gz: bcbcad67274883a3cb3d4405d7d43015c5c154241dab9c1d136ccbcae7ea53bf9e6606d4713761b7c565826487f042c0e869ecc14eafacfa46ce3bfb9de6dc26
7
- data.tar.gz: 56b86db61ba722ebbe770f0f0be2f20eaea6a132af9a3d9e5f688a2c2138d3782d92a3a425ff47b0e1881539c7bfe8135c18444b887665105edc385d3f3aa3a7
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
- All three strategies (`:advisory`, `:skiplocked`, `:hybrid`) can coexist safely during a rolling deploy — each strategy excludes jobs that are already locked by another worker regardless of which strategy that worker uses.
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(set_value)
143
- kwargs_value.present? ? configured_job.perform_later(*args_value, **kwargs_value) : configured_job.perform_later(*args_value)
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 = job_class_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 job_class_value
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 args_value
247
+ def resolve_args
250
248
  value = args || []
251
249
  value.respond_to?(:call) ? value.call : value
252
250
  end
253
251
 
254
- def kwargs_value
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
- advisory_lock(select_limit: select_limit) do |records|
24
- record = records.first
25
- begin
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
- raise ArgumentError, "Must provide a block" unless block
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
- raise ArgumentError, "Must provide a block" unless block
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
- lease_connection # sticky connection; advisory lock must outlive this statement
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(lease_connection, record.lockable_column_key, cte: true) if record
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
- if record
109
- record.advisory_unlock
110
- record.update(locked_by_id: nil, locked_at: nil, lock_type: nil) if !record.destroyed? && record.locked_by_id.present?
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
- record&.run_callbacks(:perform_unlocked)
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
@@ -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
- <div class="small">
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
- <%= tag.pre JSON.pretty_generate(truncate_display_value(execution.display_serialized_params)) %>
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.serialized_params["arguments"].map { |argument| truncate_display_value(argument).inspect }.join(', '), class: 'text-wrap text-break' %>
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)) %>
@@ -119,25 +119,25 @@
119
119
  {
120
120
  "warning_type": "SQL Injection",
121
121
  "warning_code": 0,
122
- "fingerprint": "e3cc5489d3e3e81f35347b7530d5743fbac8491ab612ceec0d034e22656269ed",
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": 102,
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 (\\n SELECT id FROM candidate\\n WHERE pg_try_advisory_lock(#{\"('x' || substr(md5(#{_quoted_table_name_string} || '-' || id::text), 1, 16))::bit(64)::bigint\"})\\n) 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]])",
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": "with_hybrid_lock_claim"
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 name is quoted via quoted_table_name (Rails); advisory lock expression uses _quoted_table_name_string (escaped); candidate SQL is Rails-generated; values are parameterized bindings."
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"
@@ -37,17 +37,17 @@ pt-BR:
37
37
  no_batches_found: Nenhum lote encontrado.
38
38
  cleaner:
39
39
  index:
40
- all: All
41
- class: Class
42
- exception: Exception
43
- grouped_by_class: Discards grouped by Class
44
- grouped_by_exception: Discards grouped by Exception
45
- last_1_hour: Last 1 hour
46
- last_24_hours: Last 24 hours
47
- last_3_days: Last 3 days
48
- last_3_hours: Last 3 hours
49
- last_7_days: Last 7 days
50
- title: Discard Cleaner
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 excluida.
145
+ notice: A tarefa foi excluída.
146
146
  discard:
147
147
  notice: A tarefa foi descartada.
148
148
  executions:
149
- application_trace: Application Trace
150
- full_trace: 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 tudo
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: Reivindicações de concorrência
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 do trabalho
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 do trabalho
249
- jobs: Trabalhos
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: "~%{count} mês"
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: Discard Cleaner
321
- cron_schedules: Agendamentos
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 uma Boa Tarefa!
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: redirect(path: 'jobs')
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],