good_job 4.22.0 → 4.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -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/good_job/batch_record.rb +0 -1
- data/app/models/good_job/concurrency_claim.rb +1 -1
- data/app/models/good_job/process.rb +1 -0
- data/app/views/good_job/batches/_jobs.erb +9 -9
- data/app/views/good_job/jobs/_table.erb +10 -10
- data/config/brakeman.ignore +3 -3
- data/lib/good_job/active_job_extensions/concurrency.rb +9 -2
- data/lib/good_job/adapter.rb +65 -22
- data/lib/good_job/configuration.rb +10 -1
- data/lib/good_job/engine.rb +4 -0
- data/lib/good_job/log_subscriber.rb +9 -0
- data/lib/good_job/ractors.rb +97 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +68 -9
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ce4bd5cfbb7e4fcc44f90099bc1aa892ef59e1294c69eac5d4279c3968db9c0
|
|
4
|
+
data.tar.gz: '093ba2cc69d8d51f38bbf4cd93d9bf97aa65d995768f00a6b0d27dbe1aacdd08'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcbcad67274883a3cb3d4405d7d43015c5c154241dab9c1d136ccbcae7ea53bf9e6606d4713761b7c565826487f042c0e869ecc14eafacfa46ce3bfb9de6dc26
|
|
7
|
+
data.tar.gz: 56b86db61ba722ebbe770f0f0be2f20eaea6a132af9a3d9e5f688a2c2138d3782d92a3a425ff47b0e1881539c7bfe8135c18444b887665105edc385d3f3aa3a7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v4.22.1](https://github.com/bensheldon/good_job/tree/v4.22.1) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.0...v4.22.1)
|
|
6
|
+
|
|
7
|
+
**Implemented enhancements:**
|
|
8
|
+
|
|
9
|
+
- 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))
|
|
10
|
+
|
|
11
|
+
**Fixed bugs:**
|
|
12
|
+
|
|
13
|
+
- 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))
|
|
14
|
+
- 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))
|
|
15
|
+
|
|
16
|
+
**Merged pull requests:**
|
|
17
|
+
|
|
18
|
+
- Support enqueuing jobs from non-main Ractors [\#1863](https://github.com/bensheldon/good_job/pull/1863) ([bensheldon](https://github.com/bensheldon))
|
|
19
|
+
|
|
3
20
|
## [v4.22.0](https://github.com/bensheldon/good_job/tree/v4.22.0) (2026-10-09)
|
|
4
21
|
|
|
5
22
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v4.21.1...v4.22.0)
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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 %>
|
|
@@ -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 %>
|
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",
|
|
@@ -6,6 +6,8 @@ module GoodJob
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
VALID_TYPES = [String, Symbol, Numeric, Date, Time, TrueClass, FalseClass, NilClass].freeze
|
|
9
|
+
# The lock and claim key for a rule without a label or key, which counts all jobs.
|
|
10
|
+
ALL_KEY = "all"
|
|
9
11
|
|
|
10
12
|
class ConcurrencyExceededError < StandardError
|
|
11
13
|
def backtrace
|
|
@@ -52,6 +54,9 @@ module GoodJob
|
|
|
52
54
|
|
|
53
55
|
def evaluate(job, stage)
|
|
54
56
|
resolved_label = resolve_label(job)
|
|
57
|
+
# A rule with a label that resolves to blank does not apply
|
|
58
|
+
return nil if @label.present? && resolved_label.blank?
|
|
59
|
+
|
|
55
60
|
resolved_key = resolve_key(job, resolved_label)
|
|
56
61
|
return nil if resolved_key.blank? && resolved_label.blank?
|
|
57
62
|
|
|
@@ -77,8 +82,10 @@ module GoodJob
|
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def resolve_key(job, label)
|
|
80
|
-
if label.present?
|
|
85
|
+
if label.present?
|
|
81
86
|
"label:#{label}"
|
|
87
|
+
elsif key.blank?
|
|
88
|
+
ALL_KEY
|
|
82
89
|
else
|
|
83
90
|
key_value = @key.respond_to?(:call) ? job.instance_exec(&@key) : @key
|
|
84
91
|
raise TypeError, "Concurrency key must be a String; was a #{key_value.class}" if key_value.present? && VALID_TYPES.none? { |type| key_value.is_a?(type) }
|
|
@@ -117,7 +124,7 @@ module GoodJob
|
|
|
117
124
|
elsif key_explicit? && key.present?
|
|
118
125
|
"key:#{key}"
|
|
119
126
|
else
|
|
120
|
-
|
|
127
|
+
ALL_KEY
|
|
121
128
|
end
|
|
122
129
|
end
|
|
123
130
|
|
data/lib/good_job/adapter.rb
CHANGED
|
@@ -11,6 +11,13 @@ module GoodJob
|
|
|
11
11
|
# @return [Array<GoodJob::Adapter>, nil]
|
|
12
12
|
cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
|
|
13
13
|
|
|
14
|
+
# Mutable per-adapter state (the capsule and whether it was started), kept
|
|
15
|
+
# off the adapter instance so that the adapter itself can be made
|
|
16
|
+
# Ractor-shareable. Only accessed from the main Ractor.
|
|
17
|
+
Runtime = Struct.new(:capsule, :async_started)
|
|
18
|
+
RUNTIMES = Concurrent::Map.new
|
|
19
|
+
private_constant :Runtime, :RUNTIMES
|
|
20
|
+
|
|
14
21
|
# @param execution_mode [Symbol, nil] specifies how and where jobs should be executed. You can also set this with the environment variable +GOOD_JOB_EXECUTION_MODE+.
|
|
15
22
|
#
|
|
16
23
|
# - +:inline+ executes jobs immediately in whatever process queued them (usually the web server process). This should only be used in test and development environments.
|
|
@@ -28,7 +35,7 @@ module GoodJob
|
|
|
28
35
|
def initialize(execution_mode: nil, _capsule: GoodJob.capsule) # rubocop:disable Lint/UnderscorePrefixedVariableName
|
|
29
36
|
@_execution_mode_override = execution_mode
|
|
30
37
|
GoodJob::Configuration.validate_execution_mode(@_execution_mode_override) if @_execution_mode_override
|
|
31
|
-
|
|
38
|
+
RUNTIMES[self] = Runtime.new(_capsule, false)
|
|
32
39
|
|
|
33
40
|
start_async if GoodJob.async_ready?
|
|
34
41
|
self.class.instances << self
|
|
@@ -75,16 +82,19 @@ module GoodJob
|
|
|
75
82
|
lock_id = nil
|
|
76
83
|
|
|
77
84
|
if execute_inline?
|
|
78
|
-
|
|
85
|
+
lock_id = register_tracker
|
|
79
86
|
tracker_registered = true
|
|
80
|
-
lock_id = @capsule.tracker.id_for_lock
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
begin
|
|
84
90
|
GoodJob::Job.transaction(requires_new: true, joinable: false) do
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
results = if GoodJob::Ractors.main?
|
|
92
|
+
column_names = GoodJob::Job.column_names
|
|
93
|
+
job_attributes = jobs.map { |job| job.attributes.slice(*column_names) }
|
|
94
|
+
GoodJob::Job.insert_all(job_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
|
|
95
|
+
else
|
|
96
|
+
insert_individually(jobs)
|
|
97
|
+
end
|
|
88
98
|
|
|
89
99
|
job_id_to_provider_job_id = results.to_h { |result| [result['active_job_id'], result['id']] }
|
|
90
100
|
active_jobs.each do |active_job|
|
|
@@ -120,16 +130,16 @@ module GoodJob
|
|
|
120
130
|
end
|
|
121
131
|
ensure
|
|
122
132
|
inline_jobs.each(&:advisory_unlock)
|
|
123
|
-
|
|
133
|
+
unregister_tracker if tracker_registered
|
|
124
134
|
tracker_registered = false
|
|
125
135
|
end
|
|
126
136
|
elsif tracker_registered
|
|
127
|
-
|
|
137
|
+
unregister_tracker
|
|
128
138
|
tracker_registered = false
|
|
129
139
|
end
|
|
130
140
|
rescue StandardError
|
|
131
141
|
if tracker_registered
|
|
132
|
-
|
|
142
|
+
unregister_tracker
|
|
133
143
|
tracker_registered = false
|
|
134
144
|
end
|
|
135
145
|
raise
|
|
@@ -147,7 +157,7 @@ module GoodJob
|
|
|
147
157
|
state = { queue_name: queue_name, count: jobs_by_queue_and_scheduled_at.size }
|
|
148
158
|
state[:scheduled_at] = scheduled_at if scheduled_at
|
|
149
159
|
|
|
150
|
-
executed_locally = execute_async? &&
|
|
160
|
+
executed_locally = execute_async? && create_capsule_thread(state)
|
|
151
161
|
unless executed_locally
|
|
152
162
|
state[:count] = job_id_to_active_jobs.values_at(*jobs_by_queue_and_scheduled_at.map(&:active_job_id)).count { |active_job| send_notify?(active_job) }
|
|
153
163
|
Notifier.notify(state) unless state[:count].zero?
|
|
@@ -183,8 +193,7 @@ module GoodJob
|
|
|
183
193
|
)
|
|
184
194
|
elsif will_execute_inline
|
|
185
195
|
lock_strategy = GoodJob::Job.effective_lock_strategy
|
|
186
|
-
|
|
187
|
-
lock_id = @capsule.tracker.id_for_lock
|
|
196
|
+
lock_id = register_tracker
|
|
188
197
|
tracker_registered = true
|
|
189
198
|
begin
|
|
190
199
|
job = case lock_strategy
|
|
@@ -198,12 +207,12 @@ module GoodJob
|
|
|
198
207
|
InlineBuffer.perform_now_or_defer do
|
|
199
208
|
perform_inline(job, notify: send_notify?(active_job), already_claimed: lock_strategy != :advisory, advisory_unlock: lock_strategy != :skiplocked)
|
|
200
209
|
ensure
|
|
201
|
-
|
|
210
|
+
unregister_tracker if tracker_registered
|
|
202
211
|
tracker_registered = false
|
|
203
212
|
end
|
|
204
213
|
rescue StandardError
|
|
205
214
|
if tracker_registered
|
|
206
|
-
|
|
215
|
+
unregister_tracker
|
|
207
216
|
tracker_registered = false
|
|
208
217
|
end
|
|
209
218
|
raise
|
|
@@ -215,7 +224,7 @@ module GoodJob
|
|
|
215
224
|
)
|
|
216
225
|
|
|
217
226
|
if job
|
|
218
|
-
executed_locally = execute_async? &&
|
|
227
|
+
executed_locally = execute_async? && create_capsule_thread(job.job_state)
|
|
219
228
|
Notifier.notify(job.job_state) if !executed_locally && send_notify?(active_job)
|
|
220
229
|
end
|
|
221
230
|
end
|
|
@@ -246,8 +255,9 @@ module GoodJob
|
|
|
246
255
|
# * A positive number will wait that many seconds before stopping any remaining active threads.
|
|
247
256
|
# @return [void]
|
|
248
257
|
def shutdown(timeout: NONE)
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
runtime = RUNTIMES[self]
|
|
259
|
+
runtime.capsule&.shutdown(timeout: timeout)
|
|
260
|
+
runtime.async_started = false
|
|
251
261
|
end
|
|
252
262
|
|
|
253
263
|
# This adapter's execution mode
|
|
@@ -282,19 +292,52 @@ module GoodJob
|
|
|
282
292
|
def start_async
|
|
283
293
|
return unless execute_async?
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
runtime = RUNTIMES[self]
|
|
296
|
+
runtime.capsule.start
|
|
297
|
+
runtime.capsule.lower_thread_priority = true if GoodJob.configuration.lower_thread_priority.in?([true, nil])
|
|
298
|
+
runtime.async_started = true
|
|
288
299
|
end
|
|
289
300
|
|
|
290
301
|
# Whether the async executors are running
|
|
291
302
|
# @return [Boolean]
|
|
292
303
|
def async_started?
|
|
293
|
-
|
|
304
|
+
GoodJob::Ractors.on_main(self) { RUNTIMES[self].async_started }
|
|
294
305
|
end
|
|
295
306
|
|
|
296
307
|
private
|
|
297
308
|
|
|
309
|
+
# The capsule is not Ractor-shareable, so it is always used on the main Ractor.
|
|
310
|
+
def create_capsule_thread(state)
|
|
311
|
+
job_state = GoodJob::Ractors.main? ? state : GoodJob::Ractors.make_shareable(state.dup)
|
|
312
|
+
GoodJob::Ractors.on_main(self) { RUNTIMES[self].capsule&.create_thread(job_state) }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def register_tracker
|
|
316
|
+
GoodJob::Ractors.on_main(self) do
|
|
317
|
+
tracker = RUNTIMES[self].capsule.tracker
|
|
318
|
+
tracker.register
|
|
319
|
+
tracker.id_for_lock
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def unregister_tracker
|
|
324
|
+
GoodJob::Ractors.on_main(self) { RUNTIMES[self].capsule.tracker.unregister }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def tracker_id_for_lock
|
|
328
|
+
GoodJob::Ractors.on_main(self) { RUNTIMES[self].capsule.tracker.id_for_lock }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Active Record does not support +insert_all+ outside of the main Ractor.
|
|
332
|
+
def insert_individually(jobs)
|
|
333
|
+
jobs.filter_map do |job|
|
|
334
|
+
GoodJob::Job.transaction(requires_new: true) { job.save!(validate: false) }
|
|
335
|
+
{ 'id' => job.id, 'active_job_id' => job.active_job_id }
|
|
336
|
+
rescue ActiveRecord::RecordNotUnique
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
298
341
|
def send_notify?(active_job)
|
|
299
342
|
return false unless GoodJob.configuration.enable_listen_notify
|
|
300
343
|
return true unless active_job.respond_to?(:good_job_notify)
|
|
@@ -309,7 +352,7 @@ module GoodJob
|
|
|
309
352
|
retried_job = nil
|
|
310
353
|
|
|
311
354
|
loop do
|
|
312
|
-
result = job.perform(lock_id:
|
|
355
|
+
result = job.perform(lock_id: tracker_id_for_lock, already_claimed: already_claimed)
|
|
313
356
|
already_claimed = false # only skip lock-write on first iteration
|
|
314
357
|
retried_job = result.retried_job
|
|
315
358
|
break if retried_job.nil? || retried_job.scheduled_at.nil? || retried_job.scheduled_at > Time.current
|
|
@@ -528,6 +528,15 @@ module GoodJob
|
|
|
528
528
|
(options[:dequeue_query_sort] || rails_config[:dequeue_query_sort] || :created_at).to_sym
|
|
529
529
|
end
|
|
530
530
|
|
|
531
|
+
# Resolves lazily memoized values so the configuration can be frozen.
|
|
532
|
+
# @return [self]
|
|
533
|
+
def prepare_for_sharing
|
|
534
|
+
in_webserver?
|
|
535
|
+
validator
|
|
536
|
+
@_rails_config = rails_config
|
|
537
|
+
self
|
|
538
|
+
end
|
|
539
|
+
|
|
531
540
|
private
|
|
532
541
|
|
|
533
542
|
# Reads a configuration value from one or more deprecated sources, emitting a
|
|
@@ -586,7 +595,7 @@ module GoodJob
|
|
|
586
595
|
end
|
|
587
596
|
|
|
588
597
|
def rails_config
|
|
589
|
-
Rails.application.config.good_job
|
|
598
|
+
@_rails_config || Rails.application.config.good_job
|
|
590
599
|
end
|
|
591
600
|
end
|
|
592
601
|
end
|
data/lib/good_job/engine.rb
CHANGED
|
@@ -10,6 +10,10 @@ module GoodJob
|
|
|
10
10
|
config.good_job = ActiveSupport::OrderedOptions.new
|
|
11
11
|
config.good_job.cron = {}
|
|
12
12
|
|
|
13
|
+
initializer "good_job.ractors" do |app|
|
|
14
|
+
app.singleton_class.prepend(GoodJob::Ractors::ApplicationExtension) if GoodJob::Ractors.enabled? && app.respond_to?(:ractorize!)
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
initializer "good_job.logger" do |_app|
|
|
14
18
|
ActiveSupport.on_load(:good_job) do
|
|
15
19
|
self.logger = ::Rails.logger if GoodJob.logger == GoodJob::DEFAULT_LOGGER
|
|
@@ -316,6 +316,15 @@ module GoodJob
|
|
|
316
316
|
def reset_logger
|
|
317
317
|
@_logger = nil
|
|
318
318
|
end
|
|
319
|
+
|
|
320
|
+
# Builds {LogSubscriber.logger} so that it can be read from non-main
|
|
321
|
+
# Ractors. A single logger is used directly because
|
|
322
|
+
# +ActiveSupport::BroadcastLogger+ is not Ractor-shareable.
|
|
323
|
+
# @return [void]
|
|
324
|
+
def prepare_for_sharing
|
|
325
|
+
@_loggers = GoodJob::Ractors.make_shareable(loggers)
|
|
326
|
+
@_logger = loggers.size == 1 ? loggers.first : GoodJob::Ractors.try_make_shareable(logger)
|
|
327
|
+
end
|
|
319
328
|
end
|
|
320
329
|
|
|
321
330
|
private
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob
|
|
4
|
+
# Compatibility layer for running GoodJob in a Ractor-ized Rails application.
|
|
5
|
+
#
|
|
6
|
+
# Delegates to +ActiveSupport::Ractors+ when the Rails version provides it,
|
|
7
|
+
# and otherwise falls back to behavior identical to a single-Ractor process.
|
|
8
|
+
# All Ractor-related calls in GoodJob should go through this module so that
|
|
9
|
+
# older Rails versions are unaffected and changes to Rails' (internal)
|
|
10
|
+
# Ractor API are contained in one place.
|
|
11
|
+
module Ractors
|
|
12
|
+
NATIVE = begin
|
|
13
|
+
require "active_support/ractors"
|
|
14
|
+
ActiveSupport::Ractors.respond_to?(:on_main) && ActiveSupport::Ractors.respond_to?(:store_if_absent)
|
|
15
|
+
rescue LoadError
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Prepended to the Rails application's singleton class so that +Rails.application.ractorize!+
|
|
20
|
+
# also makes GoodJob's global state shareable.
|
|
21
|
+
module ApplicationExtension
|
|
22
|
+
def ractorize!
|
|
23
|
+
GoodJob.ractorize!
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
LOCAL_STORAGE = Concurrent::Map.new
|
|
29
|
+
private_constant :LOCAL_STORAGE
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Whether the current Rails version supports Ractors.
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def enabled?
|
|
35
|
+
NATIVE
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether the current Ractor is the main Ractor.
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def main?
|
|
41
|
+
NATIVE ? ActiveSupport::Ractors.main? : true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Deep-freezes +obj+ so it can be shared across Ractors.
|
|
45
|
+
# @return [Object]
|
|
46
|
+
def make_shareable(obj)
|
|
47
|
+
NATIVE ? ActiveSupport::Ractors.make_shareable(obj) : obj
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Attempts to make +obj+, which may contain user-provided procs,
|
|
51
|
+
# shareable. If it cannot be made shareable, the application's
|
|
52
|
+
# +ActiveSupport::Ractors.unshareable_proc_action+ decides whether to
|
|
53
|
+
# raise, warn, or return it unchanged.
|
|
54
|
+
# @return [Object]
|
|
55
|
+
def try_make_shareable(obj)
|
|
56
|
+
return obj unless NATIVE
|
|
57
|
+
|
|
58
|
+
ActiveSupport::Ractors.make_shareable(obj)
|
|
59
|
+
rescue Ractor::IsolationError
|
|
60
|
+
raise unless ActiveSupport::Ractors.unshareable_proc_action
|
|
61
|
+
|
|
62
|
+
ActiveSupport::Ractors.try_make_shareable(obj)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Attempts to make a user-provided proc shareable, according to the
|
|
66
|
+
# application's +ActiveSupport::Ractors.unshareable_proc_action+.
|
|
67
|
+
# @return [Proc, nil]
|
|
68
|
+
def try_shareable_proc(proc)
|
|
69
|
+
return proc unless NATIVE && proc.is_a?(Proc)
|
|
70
|
+
|
|
71
|
+
ActiveSupport::Ractors.try_shareable_proc(proc)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Runs the block on the main Ractor and returns its (shareable) result.
|
|
75
|
+
# When already on the main Ractor, or Ractors are unsupported, the block
|
|
76
|
+
# is called directly.
|
|
77
|
+
def on_main(obj = nil, &block)
|
|
78
|
+
if NATIVE
|
|
79
|
+
ActiveSupport::Ractors.on_main(obj, &block)
|
|
80
|
+
else
|
|
81
|
+
obj.instance_eval(&block)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the value stored under +key+ for the current Ractor,
|
|
86
|
+
# initializing it with the block on first access. Without Ractor
|
|
87
|
+
# support, the value is process-wide.
|
|
88
|
+
def local(key, &block)
|
|
89
|
+
if NATIVE
|
|
90
|
+
ActiveSupport::Ractors.store_if_absent(key, &block)
|
|
91
|
+
else
|
|
92
|
+
LOCAL_STORAGE.compute_if_absent(key, &block)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "active_job/queue_adapters"
|
|
|
5
5
|
|
|
6
6
|
require_relative "good_job/version"
|
|
7
7
|
require_relative "good_job/engine"
|
|
8
|
+
require_relative "good_job/ractors"
|
|
8
9
|
|
|
9
10
|
require_relative "good_job/adapter"
|
|
10
11
|
require_relative "good_job/adapter/inline_buffer"
|
|
@@ -77,7 +78,8 @@ module GoodJob
|
|
|
77
78
|
# @return [ActiveRecord::Base]
|
|
78
79
|
# @example Change the base class:
|
|
79
80
|
# GoodJob.active_record_parent_class = "CustomApplicationRecord"
|
|
80
|
-
|
|
81
|
+
singleton_class.attr_accessor :active_record_parent_class
|
|
82
|
+
self.active_record_parent_class = nil
|
|
81
83
|
|
|
82
84
|
# @!attribute [rw] logger
|
|
83
85
|
# @!scope class
|
|
@@ -86,7 +88,8 @@ module GoodJob
|
|
|
86
88
|
# @return [Logger, nil]
|
|
87
89
|
# @example Output GoodJob logs to a file:
|
|
88
90
|
# GoodJob.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new("log/my_logs.log"))
|
|
89
|
-
|
|
91
|
+
singleton_class.attr_accessor :logger
|
|
92
|
+
self.logger = DEFAULT_LOGGER
|
|
90
93
|
|
|
91
94
|
# @!attribute [rw] preserve_job_records
|
|
92
95
|
# @!scope class
|
|
@@ -99,7 +102,8 @@ module GoodJob
|
|
|
99
102
|
# @example Preserve only jobs that were discarded
|
|
100
103
|
# GoodJob.preserve_job_records = ->(active_job, exception, error_event) { error_event == :discarded }
|
|
101
104
|
# @return [Boolean, Symbol, Proc, nil]
|
|
102
|
-
|
|
105
|
+
singleton_class.attr_accessor :preserve_job_records
|
|
106
|
+
self.preserve_job_records = true
|
|
103
107
|
|
|
104
108
|
# @!attribute [rw] retry_on_unhandled_error
|
|
105
109
|
# @!scope class
|
|
@@ -108,7 +112,8 @@ module GoodJob
|
|
|
108
112
|
# If +false+, jobs will be discarded or marked as finished if they raise an instance of a handled exception.
|
|
109
113
|
# @see GoodJob.handled_exceptions
|
|
110
114
|
# @return [Boolean, nil]
|
|
111
|
-
|
|
115
|
+
singleton_class.attr_accessor :retry_on_unhandled_error
|
|
116
|
+
self.retry_on_unhandled_error = false
|
|
112
117
|
|
|
113
118
|
# @!attribute [rw] handled_exceptions
|
|
114
119
|
# @!scope class
|
|
@@ -118,7 +123,8 @@ module GoodJob
|
|
|
118
123
|
# @example Also rescue ScriptError
|
|
119
124
|
# GoodJob.handled_exceptions = [StandardError, NotImplementedError, ScriptError]
|
|
120
125
|
# @return [Array<Class, String>]
|
|
121
|
-
|
|
126
|
+
singleton_class.attr_accessor :handled_exceptions
|
|
127
|
+
self.handled_exceptions = [StandardError, NotImplementedError]
|
|
122
128
|
|
|
123
129
|
# Resolved exception classes from {handled_exceptions}.
|
|
124
130
|
# @return [Array<Class>]
|
|
@@ -134,19 +140,22 @@ module GoodJob
|
|
|
134
140
|
# # config/initializers/good_job.rb
|
|
135
141
|
# GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
|
|
136
142
|
# @return [Proc, nil]
|
|
137
|
-
|
|
143
|
+
singleton_class.attr_accessor :on_thread_error
|
|
144
|
+
self.on_thread_error = nil
|
|
138
145
|
|
|
139
146
|
# @!attribute [rw] configuration
|
|
140
147
|
# @!scope class
|
|
141
148
|
# Global configuration object for GoodJob.
|
|
142
149
|
# @return [GoodJob::Configuration, nil]
|
|
143
|
-
|
|
150
|
+
singleton_class.attr_accessor :configuration
|
|
151
|
+
self.configuration = GoodJob::Configuration.new({})
|
|
144
152
|
|
|
145
153
|
# @!attribute [rw] capsule
|
|
146
154
|
# @!scope class
|
|
147
155
|
# Global/default execution capsule for GoodJob.
|
|
148
156
|
# @return [GoodJob::Capsule, nil]
|
|
149
|
-
|
|
157
|
+
singleton_class.attr_accessor :capsule
|
|
158
|
+
self.capsule = GoodJob::Capsule.new(configuration: configuration)
|
|
150
159
|
|
|
151
160
|
# Called with exception when a GoodJob thread raises an exception
|
|
152
161
|
# @param exception [Exception] Exception that was raised
|
|
@@ -167,7 +176,57 @@ module GoodJob
|
|
|
167
176
|
self._active_record_configuration = block
|
|
168
177
|
end
|
|
169
178
|
|
|
170
|
-
|
|
179
|
+
singleton_class.attr_accessor :_active_record_configuration
|
|
180
|
+
self._active_record_configuration = nil
|
|
181
|
+
|
|
182
|
+
# Makes GoodJob's global state Ractor-shareable so that jobs can be enqueued
|
|
183
|
+
# and performed from non-main Ractors. Called automatically by
|
|
184
|
+
# +Rails.application.ractorize!+ on Rails versions that support Ractors.
|
|
185
|
+
# Background execution (capsules, schedulers, the notifier) remains on the
|
|
186
|
+
# main Ractor.
|
|
187
|
+
# @return [void]
|
|
188
|
+
def self.ractorize!
|
|
189
|
+
_load_record_schemas
|
|
190
|
+
LogSubscriber.prepare_for_sharing
|
|
191
|
+
Ractors.make_shareable(configuration.prepare_for_sharing)
|
|
192
|
+
Ractors.make_shareable(handled_exceptions)
|
|
193
|
+
Ractors.make_shareable(logger)
|
|
194
|
+
self.preserve_job_records = Ractors.try_shareable_proc(preserve_job_records)
|
|
195
|
+
self.on_thread_error = Ractors.try_shareable_proc(on_thread_error)
|
|
196
|
+
_make_job_classes_shareable
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Concurrency and label settings are class attributes that may hold
|
|
200
|
+
# user-provided procs; they are frozen in place on each job class.
|
|
201
|
+
# @return [void]
|
|
202
|
+
def self._make_job_classes_shareable
|
|
203
|
+
attributes = %i[good_job_concurrency_config good_job_concurrency_rules good_job_labels]
|
|
204
|
+
[ActiveJob::Base, *ActiveJob::Base.descendants].each do |job_class|
|
|
205
|
+
attributes.each do |attribute|
|
|
206
|
+
Ractors.try_make_shareable(job_class.public_send(attribute)) if job_class.respond_to?(attribute)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Active Record schemas and GoodJob's database capability checks are
|
|
212
|
+
# lazily loaded into class-level state, which can only be written from
|
|
213
|
+
# the main Ractor.
|
|
214
|
+
# @return [void]
|
|
215
|
+
def self._load_record_schemas
|
|
216
|
+
GoodJob::AdvisoryLockable.hash_function
|
|
217
|
+
GoodJob::BaseRecord.descendants.each do |record_class|
|
|
218
|
+
next if record_class.abstract_class?
|
|
219
|
+
|
|
220
|
+
record_class.load_schema
|
|
221
|
+
record_class.define_attribute_methods
|
|
222
|
+
end
|
|
223
|
+
GoodJob::Job.lock_type_column_exists?
|
|
224
|
+
GoodJob::Job.supports_cte_materialization_specifiers?
|
|
225
|
+
GoodJob::Job.database_supports_websearch_to_tsquery?
|
|
226
|
+
GoodJob::BatchRecord.database_supports_websearch_to_tsquery?
|
|
227
|
+
rescue ActiveRecord::ActiveRecordError => e
|
|
228
|
+
logger&.warn("GoodJob could not load its database schema before ractorizing: #{e.message}")
|
|
229
|
+
end
|
|
171
230
|
|
|
172
231
|
# Stop executing jobs.
|
|
173
232
|
# GoodJob does its work in pools of background threads.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: good_job
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.22.
|
|
4
|
+
version: 4.22.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Sheldon
|
|
@@ -432,6 +432,7 @@ files:
|
|
|
432
432
|
- lib/good_job/probe_server/not_found_app.rb
|
|
433
433
|
- lib/good_job/probe_server/simple_handler.rb
|
|
434
434
|
- lib/good_job/probe_server/webrick_handler.rb
|
|
435
|
+
- lib/good_job/ractors.rb
|
|
435
436
|
- lib/good_job/safe_state.rb
|
|
436
437
|
- lib/good_job/scheduler.rb
|
|
437
438
|
- lib/good_job/sd_notify.rb
|