good_job 4.22.0 → 4.23.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +11 -1
  4. data/app/controllers/good_job/executions_controller.rb +11 -0
  5. data/app/frontend/good_job/style.css +5 -0
  6. data/app/helpers/good_job/application_helper.rb +5 -4
  7. data/app/models/concerns/good_job/advisory_lockable.rb +33 -17
  8. data/app/models/concerns/good_job/filtered_arguments.rb +27 -0
  9. data/app/models/good_job/batch_record.rb +0 -1
  10. data/app/models/good_job/concurrency_claim.rb +3 -1
  11. data/app/models/good_job/cron_entry.rb +9 -11
  12. data/app/models/good_job/execution.rb +2 -0
  13. data/app/models/good_job/job.rb +2 -0
  14. data/app/models/good_job/process.rb +1 -0
  15. data/app/views/good_job/batches/_jobs.erb +9 -9
  16. data/app/views/good_job/executions/show.html.erb +11 -0
  17. data/app/views/good_job/jobs/_executions.erb +2 -6
  18. data/app/views/good_job/jobs/_table.erb +10 -10
  19. data/app/views/good_job/jobs/show.html.erb +1 -1
  20. data/config/brakeman.ignore +3 -3
  21. data/config/locales/pt-BR.yml +25 -23
  22. data/config/routes.rb +3 -1
  23. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -0
  24. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +1 -0
  25. data/lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb +12 -0
  26. data/lib/good_job/active_job_extensions/concurrency.rb +9 -2
  27. data/lib/good_job/adapter.rb +72 -29
  28. data/lib/good_job/capsule.rb +8 -8
  29. data/lib/good_job/capsule_tracker.rb +29 -11
  30. data/lib/good_job/configuration.rb +10 -1
  31. data/lib/good_job/cron_manager.rb +5 -6
  32. data/lib/good_job/engine.rb +4 -0
  33. data/lib/good_job/instance_registry.rb +45 -0
  34. data/lib/good_job/job_performer.rb +2 -1
  35. data/lib/good_job/log_subscriber.rb +9 -0
  36. data/lib/good_job/notifier.rb +5 -6
  37. data/lib/good_job/poller.rb +4 -6
  38. data/lib/good_job/probe_server/simple_handler.rb +3 -0
  39. data/lib/good_job/probe_server/webrick_handler.rb +2 -0
  40. data/lib/good_job/probe_server.rb +2 -0
  41. data/lib/good_job/ractors.rb +99 -0
  42. data/lib/good_job/scheduler.rb +3 -6
  43. data/lib/good_job/shared_executor.rb +4 -6
  44. data/lib/good_job/systemd_service.rb +1 -0
  45. data/lib/good_job/version.rb +1 -1
  46. data/lib/good_job.rb +81 -11
  47. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3707d55ab1de17d450f3b52e30739b25350ed229e7fad992ab9f56aa01c88fc
4
- data.tar.gz: 1af86ced85ba5487a1b3c13eac8e4f2988c6685153fee444fd34afa62533ef7b
3
+ metadata.gz: 50f42b2481a308af99413d1a89c0b7d3d4c2e07cc48cc29a323c3c42a528e610
4
+ data.tar.gz: a55f5f59b5758614fa38c79b603d4f6298b6e79de40931e05fc4cc4fa0e68f56
5
5
  SHA512:
6
- metadata.gz: 56311442c755f60a717bc23c42f193f3bfc15f31254527320e9183cc681cc43c9a87d89fd5e9c2efe305d14f87f6c3cafa0d9a2e5443c5cad3eafa7da31f5674
7
- data.tar.gz: '0875bd2051120c9cd80d11e781713b2c13a6a05d2fdfa38f54dfdb0c3a616b3cbe4d23769db811211feaf7feb49cb1d2a1bf4e72baa31a9127f10b82ce18f5fd'
6
+ metadata.gz: adacf056f5e11131e0bb704e5b830a11a4a64b0d0a06339c0edaa13b1d8c94bf8b7e734e0df79d3ccf0230e7ceba3da4f220d66aa4eece5dd4dd5e961af294e1
7
+ data.tar.gz: 7b954a6a98b208038575c4d05e6f6e18d219125daf42f66ca8d1e0223f5e18eb1250fd3c608eddeb4f8b78d94ba84f3f141805ccf24f81dedeec0a37bac808ff
data/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.23.0](https://github.com/bensheldon/good_job/tree/v4.23.0) (2026-10-27)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.1...v4.23.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Filter sensitive job arguments in the Dashboard [\#1881](https://github.com/bensheldon/good_job/pull/1881) ([bogdan](https://github.com/bogdan))
10
+
11
+ **Fixed bugs:**
12
+
13
+ - Respect enable\_listen\_notify when promoting concurrency claims [\#1892](https://github.com/bensheldon/good_job/pull/1892) ([eidam](https://github.com/eidam))
14
+ - Downgrade lock\_type before releasing advisory lock [\#1883](https://github.com/bensheldon/good_job/pull/1883) ([jmsanders](https://github.com/jmsanders))
15
+
16
+ **Closed issues:**
17
+
18
+ - ConcurrencyClaim sends NOTIFY even when enable\_listen\_notify is false [\#1891](https://github.com/bensheldon/good_job/issues/1891)
19
+
20
+ **Merged pull requests:**
21
+
22
+ - Keep JRuby platform in Gemfile.lock when Bundler re-resolves [\#1893](https://github.com/bensheldon/good_job/pull/1893) ([bensheldon](https://github.com/bensheldon))
23
+ - 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))
24
+ - Pin CI test runners to ubuntu-24.04 [\#1888](https://github.com/bensheldon/good_job/pull/1888) ([bensheldon](https://github.com/bensheldon))
25
+ - Require the concurrent-ruby classes GoodJob uses [\#1885](https://github.com/bensheldon/good_job/pull/1885) ([czj](https://github.com/czj))
26
+ - Add index\_good\_job\_executions\_on\_created\_at [\#1884](https://github.com/bensheldon/good_job/pull/1884) ([clinejj](https://github.com/clinejj))
27
+ - Demo: cap YJIT memory and shrink the Rack::Attack store [\#1880](https://github.com/bensheldon/good_job/pull/1880) ([bensheldon](https://github.com/bensheldon))
28
+ - 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))
29
+ - 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))
30
+ - Return 404 for GET /jobs/mass\_update instead of redirecting [\#1877](https://github.com/bensheldon/good_job/pull/1877) ([bensheldon](https://github.com/bensheldon))
31
+ - Load execution full traces and serialized params on demand [\#1876](https://github.com/bensheldon/good_job/pull/1876) ([bensheldon](https://github.com/bensheldon))
32
+ - Add specs for the demo's Rack::Attack rules [\#1875](https://github.com/bensheldon/good_job/pull/1875) ([bensheldon](https://github.com/bensheldon))
33
+ - 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))
34
+ - Reduce demo app memory and crawler load [\#1873](https://github.com/bensheldon/good_job/pull/1873) ([bensheldon](https://github.com/bensheldon))
35
+ - Fix JRuby flake from inspect in wait\_until predicate expectations [\#1872](https://github.com/bensheldon/good_job/pull/1872) ([bensheldon](https://github.com/bensheldon))
36
+ - Remove Adapter::RUNTIMES so adapters can be garbage collected [\#1870](https://github.com/bensheldon/good_job/pull/1870) ([bensheldon](https://github.com/bensheldon))
37
+ - 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))
38
+ - Use resolve\_\[property\] naming for CronEntry configuration resolvers [\#1868](https://github.com/bensheldon/good_job/pull/1868) ([bensheldon](https://github.com/bensheldon))
39
+ - Fix inconsistencies in the Brazilian Portuguese locale [\#1867](https://github.com/bensheldon/good_job/pull/1867) ([tiagoamaro](https://github.com/tiagoamaro))
40
+
41
+ ## [v4.22.1](https://github.com/bensheldon/good_job/tree/v4.22.1) (2026-10-13)
42
+
43
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.0...v4.22.1)
44
+
45
+ **Implemented enhancements:**
46
+
47
+ - Show full job labels under the job name in dashboard tables [\#1862](https://github.com/bensheldon/good_job/pull/1862) ([olivier-thatch](https://github.com/olivier-thatch))
48
+
49
+ **Fixed bugs:**
50
+
51
+ - Keep the process id when refresh recreates a deleted process record [\#1866](https://github.com/bensheldon/good_job/pull/1866) ([wivern-co-uk](https://github.com/wivern-co-uk))
52
+ - Don't apply a concurrency lock when a rule's label resolves to blank [\#1864](https://github.com/bensheldon/good_job/pull/1864) ([bensheldon](https://github.com/bensheldon))
53
+
54
+ **Merged pull requests:**
55
+
56
+ - Support enqueuing jobs from non-main Ractors [\#1863](https://github.com/bensheldon/good_job/pull/1863) ([bensheldon](https://github.com/bensheldon))
57
+
3
58
  ## [v4.22.0](https://github.com/bensheldon/good_job/tree/v4.22.0) (2026-10-09)
4
59
 
5
60
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.21.1...v4.22.0)
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:
@@ -1644,7 +1654,7 @@ end
1644
1654
  GOOD_JOB_LOCK_STRATEGY=skiplocked
1645
1655
  ```
1646
1656
 
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.
1657
+ 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
1658
 
1649
1659
  #### PgBouncer configuration
1650
1660
 
@@ -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
@@ -186,3 +186,8 @@
186
186
  border-top-left-radius: 0 !important;
187
187
  border-bottom-left-radius: 0 !important;
188
188
  }
189
+
190
+ /* Stop a long job name from widening its cell, so label badges clip at the column edge. */
191
+ .min-width-0 {
192
+ min-width: 0;
193
+ }
@@ -27,14 +27,15 @@ module GoodJob
27
27
  # Renders a job's label as a badge. Labels on which the job has a concurrency claim
28
28
  # are colored and prefixed with an icon for the claim's state.
29
29
  # Claims are only shown when the job's +concurrency_claims+ are already loaded.
30
- def job_label_badge(job, label, url: nil)
30
+ # With +full: true+, the badge shows the full label and CSS clips it at the container width.
31
+ def job_label_badge(job, label, url: nil, full: false)
31
32
  claim = job.concurrency_claims.find { |job_claim| job_claim.label == label } if job.association(:concurrency_claims).loaded?
32
33
  state_name = claim&.state_name
33
- truncated_label = truncate(label, length: 15)
34
+ truncated_label = full ? label : truncate(label, length: 15)
34
35
  state_text = t(state_name, scope: "good_job.concurrency_claims.states") if state_name
35
- title = [(label if truncated_label != label), state_text].compact.join(" · ")
36
+ title = [(label if full || truncated_label != label), state_text].compact.join(" · ")
36
37
 
37
- options = { class: "badge font-monospace text-decoration-none #{CONCURRENCY_CLAIM_BADGE_CLASSES.fetch(state_name)}" }
38
+ options = { class: "badge font-monospace text-decoration-none #{'text-truncate mw-100 ' if full}#{CONCURRENCY_CLAIM_BADGE_CLASSES.fetch(state_name)}" }
38
39
  if title.present?
39
40
  options[:title] = title
40
41
  options[:data] = { bs_toggle: "tooltip" }
@@ -62,8 +62,22 @@ module GoodJob
62
62
  # require additional synchronization.
63
63
  #
64
64
  # Per-key structure: [session_count, xact_txns, cte]
65
- # xact_txns: Array of WeakRefs to AR transaction objects
65
+ # xact_txns: Array of TransactionRefs to AR transaction objects
66
66
  class AdvisoryLockCounter
67
+ # A weak reference to an Active Record transaction. Unlike +WeakRef+,
68
+ # which uses a class variable, this can be used from non-main Ractors.
69
+ class TransactionRef
70
+ def initialize(transaction)
71
+ @ref = ObjectSpace::WeakMap.new
72
+ @ref[:transaction] = transaction
73
+ end
74
+
75
+ def open?
76
+ transaction = @ref[:transaction]
77
+ !transaction.nil? && transaction.open?
78
+ end
79
+ end
80
+
67
81
  def initialize
68
82
  if defined?(ObjectSpace::WeakKeyMap)
69
83
  # WeakKeyMap (Ruby 3.3+) holds weak keys with strong values:
@@ -144,13 +158,13 @@ module GoodJob
144
158
 
145
159
  # Record a lock acquisition.
146
160
  # Per-key structure: [session_count, xact_txns, cte]
147
- # xact_txns is an Array of WeakRefs to transaction objects
161
+ # xact_txns is an Array of TransactionRefs
148
162
  def record_lock(conn, key, cte: false, xact: false)
149
163
  prune(conn)
150
164
  counts = self[conn] || (self[conn] = {})
151
165
  session_count, xact_txns, was_cte = counts[key] || [0, [], false]
152
166
  if xact
153
- xact_txns += [WeakRef.new(conn.current_transaction)]
167
+ xact_txns += [TransactionRef.new(conn.current_transaction)]
154
168
  else
155
169
  session_count += 1
156
170
  end
@@ -191,11 +205,7 @@ module GoodJob
191
205
  return unless counts
192
206
 
193
207
  counts.each do |key, (session_count, xact_txns, cte)|
194
- live_txns = xact_txns.select do |ref|
195
- ref.weakref_alive? && ref.open?
196
- rescue WeakRef::RefError
197
- false
198
- end
208
+ live_txns = xact_txns.select(&:open?)
199
209
 
200
210
  if session_count.zero? && live_txns.empty?
201
211
  counts.delete(key)
@@ -206,8 +216,14 @@ module GoodJob
206
216
  end
207
217
  end
208
218
 
209
- ADVISORY_LOCK_COUNTS = AdvisoryLockCounter.new
210
- private_constant :ADVISORY_LOCK_COUNTS
219
+ # Lock counts are tracked per connection, and connections are not shared
220
+ # across Ractors, so each Ractor has its own counter.
221
+ # @api private
222
+ # @return [AdvisoryLockCounter]
223
+ def self.advisory_lock_counts
224
+ GoodJob::Ractors.local(:good_job_advisory_lock_counts) { AdvisoryLockCounter.new }
225
+ end
226
+
211
227
  AREL_TABLE_NEW_KWARGS = Arel::Table.instance_method(:initialize).parameters.any? { |type, name| type == :key && name == :name }
212
228
 
213
229
  included do
@@ -447,7 +463,7 @@ module GoodJob
447
463
  # @return [Boolean] whether the lock was released.
448
464
  def advisory_unlock_key(key, function: advisory_unlockable_function, connection: nil)
449
465
  conn = connection || lease_connection
450
- session_count, _xact_count, cte = ADVISORY_LOCK_COUNTS.counts_for(conn, key)
466
+ session_count, _xact_count, cte = AdvisoryLockable.advisory_lock_counts.counts_for(conn, key)
451
467
 
452
468
  if session_count <= 1 && cte
453
469
  advisory_unlock_key!(key, function: function, connection: conn)
@@ -474,7 +490,7 @@ module GoodJob
474
490
  # @return [void]
475
491
  def advisory_unlock_key!(key, function: advisory_unlockable_function, connection: nil)
476
492
  conn = connection || lease_connection
477
- _, xact_count, = ADVISORY_LOCK_COUNTS.counts_for(conn, key)
493
+ _, xact_count, = AdvisoryLockable.advisory_lock_counts.counts_for(conn, key)
478
494
 
479
495
  if xact_count.positive?
480
496
  # An active xact lock on the same key means owns_advisory_lock_key? will
@@ -617,15 +633,15 @@ module GoodJob
617
633
  end
618
634
 
619
635
  def record_advisory_lock(conn, key, cte: false, xact: false)
620
- ADVISORY_LOCK_COUNTS.record_lock(conn, key, cte: cte, xact: xact)
636
+ AdvisoryLockable.advisory_lock_counts.record_lock(conn, key, cte: cte, xact: xact)
621
637
  end
622
638
 
623
639
  def record_advisory_unlock(conn, key)
624
- ADVISORY_LOCK_COUNTS.record_unlock(conn, key)
640
+ AdvisoryLockable.advisory_lock_counts.record_unlock(conn, key)
625
641
  end
626
642
 
627
643
  def prune_advisory_locks(conn)
628
- ADVISORY_LOCK_COUNTS.prune(conn)
644
+ AdvisoryLockable.advisory_lock_counts.prune(conn)
629
645
  end
630
646
 
631
647
  # Postgres advisory unlocking function for the class
@@ -643,7 +659,7 @@ module GoodJob
643
659
  # @return [void]
644
660
  def advisory_unlock_session(connection: nil)
645
661
  conn = connection || lease_connection
646
- ADVISORY_LOCK_COUNTS.clear_session_locks(conn)
662
+ AdvisoryLockable.advisory_lock_counts.clear_session_locks(conn)
647
663
  conn.exec_query("SELECT pg_advisory_unlock_all()::text AS unlocked", 'GoodJob::Lockable Unlock Session').first[:unlocked]
648
664
  end
649
665
 
@@ -745,7 +761,7 @@ module GoodJob
745
761
  def advisory_lock_active_on?(connection, key: lockable_key)
746
762
  return false unless connection&.active?
747
763
 
748
- ADVISORY_LOCK_COUNTS.counts_for(connection, key).first.positive?
764
+ AdvisoryLockable.advisory_lock_counts.counts_for(connection, key).first.positive?
749
765
  end
750
766
 
751
767
  # Releases all advisory locks on the record that are held by the current
@@ -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
@@ -11,7 +11,6 @@ module GoodJob
11
11
  self.implicit_order_column = 'created_at'
12
12
 
13
13
  has_many :jobs, class_name: 'GoodJob::Job', inverse_of: :batch, foreign_key: :batch_id, dependent: nil
14
- has_many :executions, class_name: 'GoodJob::Execution', foreign_key: :batch_id, inverse_of: :batch, dependent: nil
15
14
  has_many :callback_jobs, class_name: 'GoodJob::Job', foreign_key: :batch_callback_id, dependent: nil # rubocop:disable Rails/InverseOf
16
15
 
17
16
  scope :finished, -> { where.not(finished_at: nil) }
@@ -127,7 +127,7 @@ module GoodJob
127
127
  def upsert_claim(key:, job_id:, locked_by_id:, state:)
128
128
  # Raw SQL because `upsert(update_only:)` requires Rails 7.0+
129
129
  now = Time.current
130
- lease_connection.exec_update(sanitize_sql_array([<<~SQL.squish, key, job_id, locked_by_id, state, now, now]))
130
+ lease_connection.update(sanitize_sql_array([<<~SQL.squish, key, job_id, locked_by_id, state, now, now]))
131
131
  INSERT INTO #{quoted_table_name} (key, job_id, locked_by_id, state, created_at, updated_at)
132
132
  VALUES (?, ?, ?, ?, ?, ?)
133
133
  ON CONFLICT (key, job_id)
@@ -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
@@ -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
@@ -137,6 +137,7 @@ module GoodJob # :nodoc:
137
137
  rescue ActiveRecord::RecordNotFound
138
138
  @new_record = true
139
139
  self.created_at = self.updated_at = nil
140
+ id_will_change!
140
141
  state_will_change!
141
142
  save
142
143
  end
@@ -2,10 +2,9 @@
2
2
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
3
3
  <header class="list-group-item bg-body-tertiary">
4
4
  <div class="row small text-muted text-uppercase align-items-center">
5
- <div class="col-4"><%= t("good_job.models.batch.jobs") %></div>
5
+ <div class="col-5"><%= t("good_job.models.batch.jobs") %></div>
6
6
  <div class="d-none d-lg-block col-lg-1 text-lg-center"><%= t "good_job.models.job.queue" %></div>
7
7
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.priority" %></div>
8
- <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.labels" %></div>
9
8
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.attempts" %></div>
10
9
  <div class="col text-end">
11
10
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
@@ -22,9 +21,16 @@
22
21
  <% jobs.each do |job| %>
23
22
  <div role="row" class="list-group-item list-group-item-action py-3">
24
23
  <div class="row align-items-center">
25
- <div class="col-lg-4">
24
+ <div class="col-lg-5">
26
25
  <%= tag.code link_to(job.id, job_path(job), class: "small text-muted text-decoration-none") %>
27
26
  <%= tag.h5 tag.code(link_to(job.display_name, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
27
+ <% if job.labels.present? %>
28
+ <div class="mt-1 d-flex flex-wrap gap-1" aria-label="<%= t "good_job.models.job.labels" %>" role="group">
29
+ <% job.labels.each do |label| %>
30
+ <%= job_label_badge(job, label, full: true) %>
31
+ <% end %>
32
+ </div>
33
+ <% end %>
28
34
  </div>
29
35
  <div class="col-4 col-lg-1 text-lg-center">
30
36
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.queue" %></div>
@@ -34,12 +40,6 @@
34
40
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.priority" %></div>
35
41
  <span class="font-monospace fw-bold"><%= job.priority %></span>
36
42
  </div>
37
- <div class="col-4 col-lg-1 text-lg-end">
38
- <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.labels" %></div>
39
- <% job.labels&.each do |label| %>
40
- <%= job_label_badge(job, label) %>
41
- <% end %>
42
- </div>
43
43
  <div class="col-4 col-lg-1 text-lg-end">
44
44
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.attempts" %></div>
45
45
  <% if job.error %>
@@ -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>
@@ -6,7 +6,7 @@
6
6
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
7
7
  <header class="list-group-item bg-body-tertiary">
8
8
  <div class="row small text-muted text-uppercase align-items-center">
9
- <div class="col-lg-4 d-flex gap-2 flex-wrap">
9
+ <div class="col-lg-5 d-flex gap-2 flex-wrap">
10
10
  <div class="form-check d-flex flex-row px-0 mb-0">
11
11
  <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-target": "all", action: "change->checkbox-toggle#toggleAll" }) %>
12
12
  <%= label_tag('toggle_job_ids', t(".toggle_all_jobs"), class: "visually-hidden") %>
@@ -53,7 +53,6 @@
53
53
  </div>
54
54
  <div class="d-none d-lg-block col-lg-1 text-lg-center"><%= t "good_job.models.job.queue" %></div>
55
55
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.priority" %></div>
56
- <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.labels" %></div>
57
56
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.attempts" %></div>
58
57
  <div class="col text-end">
59
58
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
@@ -80,11 +79,18 @@
80
79
  <% jobs.each do |job| %>
81
80
  <%= label_tag dom_id(job, :checkbox), id: dom_id(job), role: "row", class: "list-group-item list-group-item-action py-3" do %>
82
81
  <div class="row align-items-center">
83
- <div class="col-lg-4 d-flex">
82
+ <div class="col-lg-5 d-flex">
84
83
  <%= check_box_tag 'job_ids[]', job.id, false, id: dom_id(job, :checkbox), data: { "checkbox-toggle-target": "each", action: "change->checkbox-toggle#syncAll" } %>
85
- <div class="ms-2">
84
+ <div class="ms-2 min-width-0">
86
85
  <%= tag.code link_to(job.id, job_path(job), class: "small text-muted text-decoration-none") %>
87
86
  <%= tag.h5 tag.code(link_to(job.display_name, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
87
+ <% if job.labels.present? %>
88
+ <div class="mt-1 d-flex flex-wrap gap-1" aria-label="<%= t "good_job.models.job.labels" %>" role="group">
89
+ <% job.labels.each do |label| %>
90
+ <%= job_label_badge(job, label, url: filter.to_params(label: label), full: true) %>
91
+ <% end %>
92
+ </div>
93
+ <% end %>
88
94
  <% if job.error %>
89
95
  <div class="mt-1 small">
90
96
  <strong class="small text-danger"><%= t "good_job.shared.error" %>:</strong>
@@ -101,12 +107,6 @@
101
107
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.priority" %></div>
102
108
  <%= job.priority %>
103
109
  </div>
104
- <div class="col-4 col-lg-1 text-wrap text-lg-end">
105
- <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.labels" %></div>
106
- <% job.labels&.each do |label| %>
107
- <%= job_label_badge(job, label, url: filter.to_params(label: label)) %>
108
- <% end %>
109
- </div>
110
110
  <div class="col-4 col-lg-1 text-lg-end">
111
111
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.attempts" %></div>
112
112
  <% if job.error %>
@@ -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)) %>
@@ -49,13 +49,13 @@
49
49
  {
50
50
  "warning_type": "Dangerous Eval",
51
51
  "warning_code": 13,
52
- "fingerprint": "54b7ac445eea2aa1735ae97fab1d841e15405a747ba0fdb577cf779286647534",
52
+ "fingerprint": "f7e62770ef61ea7bb2b3a0a8c19b3fa6f869fbaa51322cb56eb86f21b384ba5a",
53
53
  "check_name": "Evaluation",
54
54
  "message": "Dynamic string evaluated as code",
55
55
  "file": "lib/good_job/log_subscriber.rb",
56
- "line": 356,
56
+ "line": 365,
57
57
  "link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
58
- "code": "class_eval(\" def #{level}(progname = nil, tags: [], &block) # def info(progname = nil, tags: [], &block)\\n return unless logger # return unless logger\\n #\\n tag_logger(*tags) do # tag_logger(*tags) do\\n logger.#{level}(progname, &block) # logger.info(progname, &block)\\n end # end\\n end #\\n\", \"lib/good_job/log_subscriber.rb\", (356 + 1))",
58
+ "code": "class_eval(\" def #{level}(progname = nil, tags: [], &block) # def info(progname = nil, tags: [], &block)\\n return unless logger # return unless logger\\n #\\n tag_logger(*tags) do # tag_logger(*tags) do\\n logger.#{level}(progname, &block) # logger.info(progname, &block)\\n end # end\\n end #\\n\", \"lib/good_job/log_subscriber.rb\", (365 + 1))",
59
59
  "render_path": null,
60
60
  "location": {
61
61
  "type": "method",