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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +11 -1
- data/app/controllers/good_job/executions_controller.rb +11 -0
- data/app/frontend/good_job/style.css +5 -0
- data/app/helpers/good_job/application_helper.rb +5 -4
- data/app/models/concerns/good_job/advisory_lockable.rb +33 -17
- data/app/models/concerns/good_job/filtered_arguments.rb +27 -0
- data/app/models/good_job/batch_record.rb +0 -1
- data/app/models/good_job/concurrency_claim.rb +3 -1
- 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.rb +2 -0
- data/app/models/good_job/process.rb +1 -0
- data/app/views/good_job/batches/_jobs.erb +9 -9
- 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/_table.erb +10 -10
- data/app/views/good_job/jobs/show.html.erb +1 -1
- data/config/brakeman.ignore +3 -3
- 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/active_job_extensions/concurrency.rb +9 -2
- data/lib/good_job/adapter.rb +72 -29
- data/lib/good_job/capsule.rb +8 -8
- data/lib/good_job/capsule_tracker.rb +29 -11
- data/lib/good_job/configuration.rb +10 -1
- data/lib/good_job/cron_manager.rb +5 -6
- data/lib/good_job/engine.rb +4 -0
- data/lib/good_job/instance_registry.rb +45 -0
- data/lib/good_job/job_performer.rb +2 -1
- data/lib/good_job/log_subscriber.rb +9 -0
- 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 +99 -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 +81 -11
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50f42b2481a308af99413d1a89c0b7d3d4c2e07cc48cc29a323c3c42a528e610
|
|
4
|
+
data.tar.gz: a55f5f59b5758614fa38c79b603d4f6298b6e79de40931e05fc4cc4fa0e68f56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 += [
|
|
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
|
|
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
|
-
|
|
210
|
-
|
|
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 =
|
|
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, =
|
|
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
|
-
|
|
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
|
-
|
|
640
|
+
AdvisoryLockable.advisory_lock_counts.record_unlock(conn, key)
|
|
625
641
|
end
|
|
626
642
|
|
|
627
643
|
def prune_advisory_locks(conn)
|
|
628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
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
|
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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>
|
|
@@ -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-
|
|
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-
|
|
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.
|
|
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
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
{
|
|
50
50
|
"warning_type": "Dangerous Eval",
|
|
51
51
|
"warning_code": 13,
|
|
52
|
-
"fingerprint": "
|
|
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":
|
|
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\", (
|
|
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",
|