good_job 4.1.0 → 4.2.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 +71 -0
- data/README.md +10 -10
- data/app/charts/good_job/performance_index_chart.rb +1 -1
- data/app/charts/good_job/performance_show_chart.rb +1 -1
- data/app/controllers/good_job/application_controller.rb +1 -1
- data/app/controllers/good_job/batches_controller.rb +6 -0
- data/app/controllers/good_job/frontends_controller.rb +6 -2
- data/app/controllers/good_job/performance_controller.rb +1 -1
- data/app/frontend/good_job/icons.svg +79 -0
- data/app/frontend/good_job/style.css +5 -0
- data/app/helpers/good_job/icons_helper.rb +8 -5
- data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
- data/app/models/concerns/good_job/error_events.rb +2 -2
- data/app/models/concerns/good_job/reportable.rb +8 -12
- data/app/models/good_job/batch.rb +31 -9
- data/app/models/good_job/batch_record.rb +19 -20
- data/app/models/good_job/discrete_execution.rb +6 -59
- data/app/models/good_job/execution.rb +59 -4
- data/app/models/good_job/execution_result.rb +6 -6
- data/app/models/good_job/job.rb +543 -12
- data/app/models/good_job/process.rb +14 -3
- data/app/views/good_job/batches/_jobs.erb +1 -1
- data/app/views/good_job/batches/_table.erb +7 -1
- data/app/views/good_job/batches/show.html.erb +8 -0
- data/app/views/good_job/jobs/index.html.erb +1 -1
- data/app/views/layouts/good_job/application.html.erb +7 -7
- data/config/brakeman.ignore +75 -0
- data/config/locales/de.yml +54 -49
- data/config/locales/en.yml +5 -0
- data/config/locales/es.yml +19 -14
- data/config/locales/fr.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/config/locales/ja.yml +10 -5
- data/config/locales/ko.yml +9 -4
- data/config/locales/nl.yml +5 -0
- data/config/locales/pt-BR.yml +5 -0
- data/config/locales/ru.yml +5 -0
- data/config/locales/tr.yml +5 -0
- data/config/locales/uk.yml +6 -1
- data/config/routes.rb +8 -4
- data/lib/good_job/active_job_extensions/concurrency.rb +109 -98
- data/lib/good_job/adapter/inline_buffer.rb +73 -0
- data/lib/good_job/adapter.rb +59 -53
- data/lib/good_job/capsule_tracker.rb +2 -2
- data/lib/good_job/configuration.rb +13 -12
- data/lib/good_job/cron_manager.rb +1 -3
- data/lib/good_job/current_thread.rb +4 -4
- data/lib/good_job/notifier/process_heartbeat.rb +3 -2
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +6 -5
- metadata +6 -20
- data/app/models/good_job/base_execution.rb +0 -605
- data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
- data/app/views/good_job/shared/icons/_check.html.erb +0 -5
- data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
- data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
- data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
- data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
- data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
- data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
- data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
- data/app/views/good_job/shared/icons/_info.html.erb +0 -4
- data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
- data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
- data/app/views/good_job/shared/icons/_play.html.erb +0 -4
- data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
- data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
- data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
data/config/routes.rb
CHANGED
@@ -19,7 +19,11 @@ GoodJob::Engine.routes.draw do
|
|
19
19
|
get 'jobs/metrics/primary_nav', to: 'metrics#primary_nav', as: :metrics_primary_nav
|
20
20
|
get 'jobs/metrics/job_status', to: 'metrics#job_status', as: :metrics_job_status
|
21
21
|
|
22
|
-
resources :batches, only: %i[index show]
|
22
|
+
resources :batches, only: %i[index show] do
|
23
|
+
member do
|
24
|
+
put :retry
|
25
|
+
end
|
26
|
+
end
|
23
27
|
|
24
28
|
resources :cron_entries, only: %i[index show], param: :cron_key do
|
25
29
|
member do
|
@@ -33,8 +37,8 @@ GoodJob::Engine.routes.draw do
|
|
33
37
|
|
34
38
|
resources :performance, only: %i[index show]
|
35
39
|
|
36
|
-
scope :frontend, controller: :frontends do
|
37
|
-
get "modules/:
|
38
|
-
get "static/:
|
40
|
+
scope :frontend, controller: :frontends, defaults: { version: GoodJob::VERSION.tr(".", "-") } do
|
41
|
+
get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: 'js' }
|
42
|
+
get "static/:version/:id", action: :static, as: :frontend_static
|
39
43
|
end
|
40
44
|
end
|
@@ -28,28 +28,88 @@ module GoodJob
|
|
28
28
|
class_attribute :good_job_concurrency_config, instance_accessor: false, default: {}
|
29
29
|
attr_writer :good_job_concurrency_key
|
30
30
|
|
31
|
-
if ActiveJob.gem_version >= Gem::Version.new("6.1.0")
|
32
|
-
before_enqueue do |job|
|
33
|
-
good_job_enqueue_concurrency_check(job, on_abort: -> { throw(:abort) }, on_enqueue: nil)
|
34
|
-
end
|
35
|
-
else
|
36
|
-
around_enqueue do |job, block|
|
37
|
-
good_job_enqueue_concurrency_check(job, on_abort: nil, on_enqueue: block)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
31
|
wait_key = if ActiveJob.gem_version >= Gem::Version.new("7.1.0.a")
|
42
32
|
:polynomially_longer
|
43
33
|
else
|
44
34
|
:exponentially_longer
|
45
35
|
end
|
46
|
-
|
47
36
|
retry_on(
|
48
37
|
GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
|
49
38
|
attempts: Float::INFINITY,
|
50
39
|
wait: wait_key
|
51
40
|
)
|
52
41
|
|
42
|
+
before_enqueue do |job|
|
43
|
+
# Don't attempt to enforce concurrency limits with other queue adapters.
|
44
|
+
next unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
|
45
|
+
|
46
|
+
# Always allow jobs to be retried because the current job's execution will complete momentarily
|
47
|
+
next if CurrentThread.active_job_id == job.job_id
|
48
|
+
|
49
|
+
# Only generate the concurrency key on the initial enqueue in case it is dynamic
|
50
|
+
job.good_job_concurrency_key ||= job._good_job_concurrency_key
|
51
|
+
key = job.good_job_concurrency_key
|
52
|
+
next if key.blank?
|
53
|
+
|
54
|
+
enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
|
55
|
+
enqueue_limit = instance_exec(&enqueue_limit) if enqueue_limit.respond_to?(:call)
|
56
|
+
enqueue_limit = nil unless enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)
|
57
|
+
|
58
|
+
unless enqueue_limit
|
59
|
+
total_limit = job.class.good_job_concurrency_config[:total_limit]
|
60
|
+
total_limit = instance_exec(&total_limit) if total_limit.respond_to?(:call)
|
61
|
+
total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
|
62
|
+
end
|
63
|
+
|
64
|
+
enqueue_throttle = job.class.good_job_concurrency_config[:enqueue_throttle]
|
65
|
+
enqueue_throttle = instance_exec(&enqueue_throttle) if enqueue_throttle.respond_to?(:call)
|
66
|
+
enqueue_throttle = nil unless enqueue_throttle.present? && enqueue_throttle.is_a?(Array) && enqueue_throttle.size == 2
|
67
|
+
|
68
|
+
limit = enqueue_limit || total_limit
|
69
|
+
throttle = enqueue_throttle
|
70
|
+
next unless limit || throttle
|
71
|
+
|
72
|
+
exceeded = nil
|
73
|
+
GoodJob::Job.transaction(requires_new: true, joinable: false) do
|
74
|
+
GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_xact_lock") do
|
75
|
+
if limit
|
76
|
+
enqueue_concurrency = if enqueue_limit
|
77
|
+
GoodJob::Job.where(concurrency_key: key).unfinished.advisory_unlocked.count
|
78
|
+
else
|
79
|
+
GoodJob::Job.where(concurrency_key: key).unfinished.count
|
80
|
+
end
|
81
|
+
|
82
|
+
# The job has not yet been enqueued, so check if adding it will go over the limit
|
83
|
+
if (enqueue_concurrency + 1) > limit
|
84
|
+
logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
|
85
|
+
exceeded = :limit
|
86
|
+
next
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if throttle
|
91
|
+
throttle_limit = throttle[0]
|
92
|
+
throttle_period = throttle[1]
|
93
|
+
enqueued_within_period = GoodJob::Job.where(concurrency_key: key)
|
94
|
+
.where(GoodJob::Job.arel_table[:created_at].gt(throttle_period.ago))
|
95
|
+
.count
|
96
|
+
|
97
|
+
if (enqueued_within_period + 1) > throttle_limit
|
98
|
+
logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{limit} #{'job'.pluralize(limit)}"
|
99
|
+
exceeded = :throttle
|
100
|
+
next
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Rollback the transaction because it's potentially less expensive than committing it
|
106
|
+
# even though nothing has been altered in the transaction.
|
107
|
+
raise ActiveRecord::Rollback
|
108
|
+
end
|
109
|
+
|
110
|
+
throw :abort if exceeded
|
111
|
+
end
|
112
|
+
|
53
113
|
before_perform do |job|
|
54
114
|
# Don't attempt to enforce concurrency limits with other queue adapters.
|
55
115
|
next unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
|
@@ -80,30 +140,47 @@ module GoodJob
|
|
80
140
|
next
|
81
141
|
end
|
82
142
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
143
|
+
exceeded = nil
|
144
|
+
GoodJob::Job.transaction(requires_new: true, joinable: false) do
|
145
|
+
GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_xact_lock") do
|
146
|
+
if limit
|
147
|
+
allowed_active_job_ids = GoodJob::Job.unfinished.where(concurrency_key: key)
|
148
|
+
.advisory_locked
|
149
|
+
.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC"))
|
150
|
+
.limit(limit).pluck(:active_job_id)
|
151
|
+
# The current job has already been locked and will appear in the previous query
|
152
|
+
unless allowed_active_job_ids.include?(job.job_id)
|
153
|
+
exceeded = :limit
|
154
|
+
next
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
if throttle
|
159
|
+
throttle_limit = throttle[0]
|
160
|
+
throttle_period = throttle[1]
|
161
|
+
|
162
|
+
query = Execution.joins(:job)
|
163
|
+
.where(GoodJob::Job.table_name => { concurrency_key: key })
|
164
|
+
.where(Execution.arel_table[:created_at].gt(Execution.bind_value('created_at', throttle_period.ago, ActiveRecord::Type::DateTime)))
|
165
|
+
allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
|
166
|
+
.order(created_at: :asc)
|
167
|
+
.limit(throttle_limit)
|
168
|
+
.pluck(:active_job_id)
|
169
|
+
|
170
|
+
unless allowed_active_job_ids.include?(job.job_id)
|
171
|
+
exceeded = :throttle
|
172
|
+
next
|
173
|
+
end
|
174
|
+
end
|
91
175
|
end
|
92
176
|
|
93
|
-
|
94
|
-
|
95
|
-
throttle_period = throttle[1]
|
96
|
-
|
97
|
-
query = DiscreteExecution.joins(:job)
|
98
|
-
.where(GoodJob::Job.table_name => { concurrency_key: key })
|
99
|
-
.where(DiscreteExecution.arel_table[:created_at].gt(DiscreteExecution.bind_value('created_at', throttle_period.ago, ActiveRecord::Type::DateTime)))
|
100
|
-
allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
|
101
|
-
.order(created_at: :asc)
|
102
|
-
.limit(throttle_limit)
|
103
|
-
.pluck(:active_job_id)
|
177
|
+
raise ActiveRecord::Rollback
|
178
|
+
end
|
104
179
|
|
105
|
-
|
106
|
-
|
180
|
+
if exceeded == :limit
|
181
|
+
raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError
|
182
|
+
elsif exceeded == :throttle
|
183
|
+
raise GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError
|
107
184
|
end
|
108
185
|
end
|
109
186
|
end
|
@@ -139,72 +216,6 @@ module GoodJob
|
|
139
216
|
def _good_job_default_concurrency_key
|
140
217
|
self.class.name.to_s
|
141
218
|
end
|
142
|
-
|
143
|
-
private
|
144
|
-
|
145
|
-
def good_job_enqueue_concurrency_check(job, on_abort:, on_enqueue:)
|
146
|
-
# Don't attempt to enforce concurrency limits with other queue adapters.
|
147
|
-
return on_enqueue&.call unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
|
148
|
-
|
149
|
-
# Always allow jobs to be retried because the current job's execution will complete momentarily
|
150
|
-
return on_enqueue&.call if CurrentThread.active_job_id == job.job_id
|
151
|
-
|
152
|
-
# Only generate the concurrency key on the initial enqueue in case it is dynamic
|
153
|
-
job.good_job_concurrency_key ||= job._good_job_concurrency_key
|
154
|
-
key = job.good_job_concurrency_key
|
155
|
-
return on_enqueue&.call if key.blank?
|
156
|
-
|
157
|
-
enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
|
158
|
-
enqueue_limit = instance_exec(&enqueue_limit) if enqueue_limit.respond_to?(:call)
|
159
|
-
enqueue_limit = nil unless enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)
|
160
|
-
|
161
|
-
unless enqueue_limit
|
162
|
-
total_limit = job.class.good_job_concurrency_config[:total_limit]
|
163
|
-
total_limit = instance_exec(&total_limit) if total_limit.respond_to?(:call)
|
164
|
-
total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
|
165
|
-
end
|
166
|
-
|
167
|
-
enqueue_throttle = job.class.good_job_concurrency_config[:enqueue_throttle]
|
168
|
-
enqueue_throttle = instance_exec(&enqueue_throttle) if enqueue_throttle.respond_to?(:call)
|
169
|
-
enqueue_throttle = nil unless enqueue_throttle.present? && enqueue_throttle.is_a?(Array) && enqueue_throttle.size == 2
|
170
|
-
|
171
|
-
limit = enqueue_limit || total_limit
|
172
|
-
throttle = enqueue_throttle
|
173
|
-
return on_enqueue&.call unless limit || throttle
|
174
|
-
|
175
|
-
GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_lock") do
|
176
|
-
if limit
|
177
|
-
enqueue_concurrency = if enqueue_limit
|
178
|
-
GoodJob::Job.where(concurrency_key: key).unfinished.advisory_unlocked.count
|
179
|
-
else
|
180
|
-
GoodJob::Job.where(concurrency_key: key).unfinished.count
|
181
|
-
end
|
182
|
-
|
183
|
-
# The job has not yet been enqueued, so check if adding it will go over the limit
|
184
|
-
if (enqueue_concurrency + 1) > limit
|
185
|
-
logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
|
186
|
-
on_abort&.call
|
187
|
-
break
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
if throttle
|
192
|
-
throttle_limit = throttle[0]
|
193
|
-
throttle_period = throttle[1]
|
194
|
-
enqueued_within_period = GoodJob::Job.where(concurrency_key: key)
|
195
|
-
.where(GoodJob::Job.arel_table[:created_at].gt(throttle_period.ago))
|
196
|
-
.count
|
197
|
-
|
198
|
-
if (enqueued_within_period + 1) > throttle_limit
|
199
|
-
logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{limit} #{'job'.pluralize(limit)}"
|
200
|
-
on_abort&.call
|
201
|
-
break
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
on_enqueue&.call
|
206
|
-
end
|
207
|
-
end
|
208
219
|
end
|
209
220
|
end
|
210
221
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
4
|
+
|
5
|
+
module GoodJob
|
6
|
+
class Adapter
|
7
|
+
# The InlineBuffer is integrated into the Adapter and captures jobs that have been enqueued inline.
|
8
|
+
# The purpose is allow job records to be persisted, in a locked state, while within a transaction,
|
9
|
+
# and then execute the jobs after the transaction has been committed to ensure that the jobs
|
10
|
+
# do not run within a transaction.
|
11
|
+
#
|
12
|
+
# @private This is intended for internal GoodJob usage only.
|
13
|
+
class InlineBuffer
|
14
|
+
# @!attribute [rw] current_buffer
|
15
|
+
# @!scope class
|
16
|
+
# Current buffer of jobs to be enqueued.
|
17
|
+
# @return [GoodJob::Adapter::InlineBuffer, nil]
|
18
|
+
thread_mattr_accessor :current_buffer
|
19
|
+
|
20
|
+
# This block should be used to wrap the transaction that could enqueue jobs.
|
21
|
+
# @yield The block that may enqueue jobs.
|
22
|
+
# @return [Proc] A proc that will execute enqueued jobs after the transaction has been committed.
|
23
|
+
# @example Wrapping a transaction
|
24
|
+
# buffer = GoodJob::Adapter::InlineBuffer.capture do
|
25
|
+
# ActiveRecord::Base.transaction do
|
26
|
+
# MyJob.perform_later
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# buffer.call
|
30
|
+
def self.capture
|
31
|
+
if current_buffer
|
32
|
+
yield
|
33
|
+
return proc {}
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
self.current_buffer = new
|
38
|
+
yield
|
39
|
+
current_buffer.to_proc
|
40
|
+
ensure
|
41
|
+
self.current_buffer = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Used within the adapter to wrap inline job execution
|
46
|
+
def self.perform_now_or_defer(&block)
|
47
|
+
if defer?
|
48
|
+
current_buffer.defer(block)
|
49
|
+
else
|
50
|
+
yield
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.defer?
|
55
|
+
current_buffer.present?
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@callables = []
|
60
|
+
end
|
61
|
+
|
62
|
+
def defer(callable)
|
63
|
+
@callables << callable
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_proc
|
67
|
+
proc do
|
68
|
+
@callables.map(&:call)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -57,7 +57,7 @@ module GoodJob
|
|
57
57
|
|
58
58
|
Rails.application.executor.wrap do
|
59
59
|
current_time = Time.current
|
60
|
-
|
60
|
+
jobs = active_jobs.map do |active_job|
|
61
61
|
GoodJob::Job.build_for_enqueue(active_job).tap do |job|
|
62
62
|
job.scheduled_at = current_time if job.scheduled_at == job.created_at
|
63
63
|
job.created_at = current_time
|
@@ -65,62 +65,56 @@ module GoodJob
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
|
68
|
+
inline_jobs = []
|
69
69
|
GoodJob::Job.transaction(requires_new: true, joinable: false) do
|
70
|
-
|
71
|
-
results = GoodJob::Job.insert_all(
|
70
|
+
job_attributes = jobs.map(&:attributes)
|
71
|
+
results = GoodJob::Job.insert_all(job_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
|
72
72
|
|
73
73
|
job_id_to_provider_job_id = results.each_with_object({}) { |result, hash| hash[result['active_job_id']] = result['id'] }
|
74
74
|
active_jobs.each do |active_job|
|
75
75
|
active_job.provider_job_id = job_id_to_provider_job_id[active_job.job_id]
|
76
76
|
active_job.successfully_enqueued = active_job.provider_job_id.present? if active_job.respond_to?(:successfully_enqueued=)
|
77
77
|
end
|
78
|
-
|
79
|
-
|
78
|
+
jobs.each do |job|
|
79
|
+
job.instance_variable_set(:@new_record, false) if job_id_to_provider_job_id[job.active_job_id]
|
80
80
|
end
|
81
|
-
|
81
|
+
jobs = jobs.select(&:persisted?) # prune unpersisted jobs
|
82
82
|
|
83
83
|
if execute_inline?
|
84
|
-
|
85
|
-
|
84
|
+
inline_jobs = jobs.select { |job| job.scheduled_at.nil? || job.scheduled_at <= Time.current }
|
85
|
+
inline_jobs.each(&:advisory_lock!)
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
retried_execution = inline_result.retried
|
97
|
-
while retried_execution && retried_execution.scheduled_at <= Time.current
|
98
|
-
inline_execution = retried_execution
|
99
|
-
inline_result = inline_execution.perform(lock_id: @capsule.tracker.id_for_lock)
|
100
|
-
retried_execution = inline_result.retried
|
89
|
+
if inline_jobs.any?
|
90
|
+
deferred = InlineBuffer.defer?
|
91
|
+
InlineBuffer.perform_now_or_defer do
|
92
|
+
@capsule.tracker.register do
|
93
|
+
until inline_jobs.empty?
|
94
|
+
inline_job = inline_jobs.shift
|
95
|
+
perform_inline(inline_job, notify: deferred ? send_notify?(inline_job) : false)
|
101
96
|
end
|
102
97
|
ensure
|
103
|
-
|
104
|
-
inline_execution.run_callbacks(:perform_unlocked)
|
98
|
+
inline_jobs.each(&:advisory_unlock)
|
105
99
|
end
|
106
|
-
raise inline_result.unhandled_error if inline_result.unhandled_error
|
107
100
|
end
|
108
|
-
ensure
|
109
|
-
@capsule.tracker.unregister
|
110
|
-
inline_executions.each(&:advisory_unlock)
|
111
101
|
end
|
112
102
|
|
113
|
-
|
114
|
-
|
103
|
+
non_inline_jobs = if InlineBuffer.defer?
|
104
|
+
jobs - inline_jobs
|
105
|
+
else
|
106
|
+
jobs.reject(&:finished_at)
|
107
|
+
end
|
108
|
+
if non_inline_jobs.any?
|
115
109
|
job_id_to_active_jobs = active_jobs.index_by(&:job_id)
|
116
|
-
|
117
|
-
|
118
|
-
state = { queue_name: queue_name, count:
|
110
|
+
non_inline_jobs.group_by(&:queue_name).each do |queue_name, jobs_by_queue|
|
111
|
+
jobs_by_queue.group_by(&:scheduled_at).each do |scheduled_at, jobs_by_queue_and_scheduled_at|
|
112
|
+
state = { queue_name: queue_name, count: jobs_by_queue_and_scheduled_at.size }
|
119
113
|
state[:scheduled_at] = scheduled_at if scheduled_at
|
120
114
|
|
121
115
|
executed_locally = execute_async? && @capsule&.create_thread(state)
|
122
116
|
unless executed_locally
|
123
|
-
state[:count] = job_id_to_active_jobs.values_at(*
|
117
|
+
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) }
|
124
118
|
Notifier.notify(state) unless state[:count].zero?
|
125
119
|
end
|
126
120
|
end
|
@@ -148,43 +142,32 @@ module GoodJob
|
|
148
142
|
will_retry_inline = will_execute_inline && CurrentThread.job&.active_job_id == active_job.job_id && !CurrentThread.retry_now
|
149
143
|
|
150
144
|
if will_retry_inline
|
151
|
-
|
145
|
+
job = GoodJob::Job.enqueue(
|
152
146
|
active_job,
|
153
147
|
scheduled_at: scheduled_at
|
154
148
|
)
|
155
149
|
elsif will_execute_inline
|
156
|
-
|
150
|
+
job = GoodJob::Job.enqueue(
|
157
151
|
active_job,
|
158
152
|
scheduled_at: scheduled_at,
|
159
153
|
create_with_advisory_lock: true
|
160
154
|
)
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
retried_execution = result.retried
|
165
|
-
while retried_execution && (retried_execution.scheduled_at.nil? || retried_execution.scheduled_at <= Time.current)
|
166
|
-
execution = retried_execution
|
167
|
-
result = @capsule.tracker.register { execution.perform(lock_id: @capsule.tracker.id_for_lock) }
|
168
|
-
retried_execution = result.retried
|
155
|
+
InlineBuffer.perform_now_or_defer do
|
156
|
+
@capsule.tracker.register do
|
157
|
+
perform_inline(job, notify: send_notify?(active_job))
|
169
158
|
end
|
170
|
-
|
171
|
-
Notifier.notify(retried_execution.job_state) if retried_execution&.scheduled_at && retried_execution.scheduled_at > Time.current && send_notify?(active_job)
|
172
|
-
ensure
|
173
|
-
execution.advisory_unlock
|
174
|
-
execution.run_callbacks(:perform_unlocked)
|
175
159
|
end
|
176
|
-
raise result.unhandled_error if result.unhandled_error
|
177
160
|
else
|
178
|
-
|
161
|
+
job = GoodJob::Job.enqueue(
|
179
162
|
active_job,
|
180
163
|
scheduled_at: scheduled_at
|
181
164
|
)
|
182
165
|
|
183
|
-
executed_locally = execute_async? && @capsule&.create_thread(
|
184
|
-
Notifier.notify(
|
166
|
+
executed_locally = execute_async? && @capsule&.create_thread(job.job_state)
|
167
|
+
Notifier.notify(job.job_state) if !executed_locally && send_notify?(active_job)
|
185
168
|
end
|
186
169
|
|
187
|
-
|
170
|
+
job
|
188
171
|
end
|
189
172
|
end
|
190
173
|
|
@@ -250,5 +233,28 @@ module GoodJob
|
|
250
233
|
|
251
234
|
!(active_job.good_job_notify == false || (active_job.class.good_job_notify == false && active_job.good_job_notify.nil?))
|
252
235
|
end
|
236
|
+
|
237
|
+
# @param job [GoodJob::Job] the job to perform, which must be enqueued and advisory locked already
|
238
|
+
# @param notify [Boolean] whether to send a NOTIFY event for a retried job
|
239
|
+
def perform_inline(job, notify: true)
|
240
|
+
result = nil
|
241
|
+
retried_job = nil
|
242
|
+
|
243
|
+
loop do
|
244
|
+
result = job.perform(lock_id: @capsule.tracker.id_for_lock)
|
245
|
+
retried_job = result.retried_job
|
246
|
+
break if retried_job.nil? || retried_job.scheduled_at.nil? || retried_job.scheduled_at > Time.current
|
247
|
+
|
248
|
+
job = retried_job
|
249
|
+
end
|
250
|
+
|
251
|
+
Notifier.notify(retried_job.job_state) if notify && retried_job&.scheduled_at && retried_job.scheduled_at > Time.current
|
252
|
+
result
|
253
|
+
ensure
|
254
|
+
job.advisory_unlock
|
255
|
+
job.run_callbacks(:perform_unlocked)
|
256
|
+
|
257
|
+
raise result.unhandled_error if result.unhandled_error
|
258
|
+
end
|
253
259
|
end
|
254
260
|
end
|
@@ -57,7 +57,7 @@ module GoodJob # :nodoc:
|
|
57
57
|
if @record
|
58
58
|
@record.refresh_if_stale
|
59
59
|
else
|
60
|
-
@record = GoodJob::Process.
|
60
|
+
@record = GoodJob::Process.find_or_create_record(id: @record_id)
|
61
61
|
create_refresh_task
|
62
62
|
end
|
63
63
|
value = @record&.id
|
@@ -89,7 +89,7 @@ module GoodJob # :nodoc:
|
|
89
89
|
@advisory_locked_connection = WeakRef.new(@record.class.connection)
|
90
90
|
end
|
91
91
|
else
|
92
|
-
@record = GoodJob::Process.
|
92
|
+
@record = GoodJob::Process.find_or_create_record(id: @record_id, with_advisory_lock: true)
|
93
93
|
@advisory_locked_connection = WeakRef.new(@record.class.connection)
|
94
94
|
create_refresh_task
|
95
95
|
end
|
@@ -35,8 +35,6 @@ module GoodJob
|
|
35
35
|
DEFAULT_DASHBOARD_LIVE_POLL_ENABLED = true
|
36
36
|
# Default enqueue_after_transaction_commit
|
37
37
|
DEFAULT_ENQUEUE_AFTER_TRANSACTION_COMMIT = false
|
38
|
-
# Default smaller_number_is_higher_priority
|
39
|
-
DEFAULT_SMALLER_NUMBER_IS_HIGHER_PRIORITY = true
|
40
38
|
|
41
39
|
def self.validate_execution_mode(execution_mode)
|
42
40
|
raise ArgumentError, "GoodJob execution mode must be one of #{EXECUTION_MODES.join(', ')}. It was '#{execution_mode}' which is not valid." unless execution_mode.in?(EXECUTION_MODES)
|
@@ -150,11 +148,10 @@ module GoodJob
|
|
150
148
|
# poll (using this interval) for new queued jobs to execute.
|
151
149
|
# @return [Integer]
|
152
150
|
def poll_interval
|
153
|
-
interval =
|
151
|
+
interval =
|
154
152
|
options[:poll_interval] ||
|
155
|
-
|
156
|
-
|
157
|
-
)
|
153
|
+
rails_config[:poll_interval] ||
|
154
|
+
env['GOOD_JOB_POLL_INTERVAL']
|
158
155
|
|
159
156
|
if interval
|
160
157
|
interval.to_i
|
@@ -348,12 +345,6 @@ module GoodJob
|
|
348
345
|
DEFAULT_ENABLE_LISTEN_NOTIFY
|
349
346
|
end
|
350
347
|
|
351
|
-
def smaller_number_is_higher_priority
|
352
|
-
return rails_config[:smaller_number_is_higher_priority] unless rails_config[:smaller_number_is_higher_priority].nil?
|
353
|
-
|
354
|
-
DEFAULT_SMALLER_NUMBER_IS_HIGHER_PRIORITY
|
355
|
-
end
|
356
|
-
|
357
348
|
def dashboard_default_locale
|
358
349
|
rails_config[:dashboard_default_locale] || DEFAULT_DASHBOARD_DEFAULT_LOCALE
|
359
350
|
end
|
@@ -386,6 +377,16 @@ module GoodJob
|
|
386
377
|
end || false
|
387
378
|
end
|
388
379
|
|
380
|
+
# Whether to take an advisory lock on the process record in the notifier reactor.
|
381
|
+
# @return [Boolean]
|
382
|
+
def advisory_lock_heartbeat
|
383
|
+
return options[:advisory_lock_heartbeat] unless options[:advisory_lock_heartbeat].nil?
|
384
|
+
return rails_config[:advisory_lock_heartbeat] unless rails_config[:advisory_lock_heartbeat].nil?
|
385
|
+
return ActiveModel::Type::Boolean.new.cast(env['GOOD_JOB_ADVISORY_LOCK_HEARTBEAT']) unless env['GOOD_JOB_ADVISORY_LOCK_HEARTBEAT'].nil?
|
386
|
+
|
387
|
+
Rails.env.development?
|
388
|
+
end
|
389
|
+
|
389
390
|
private
|
390
391
|
|
391
392
|
def rails_config
|
@@ -55,9 +55,7 @@ module GoodJob # :nodoc:
|
|
55
55
|
# @param timeout [Numeric, nil] Unused but retained for compatibility
|
56
56
|
def shutdown(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
|
57
57
|
@running = false
|
58
|
-
@tasks.
|
59
|
-
task.cancel
|
60
|
-
end
|
58
|
+
@tasks.each_value(&:cancel)
|
61
59
|
@tasks.clear
|
62
60
|
end
|
63
61
|
|
@@ -15,7 +15,7 @@ module GoodJob
|
|
15
15
|
error_on_retry_stopped
|
16
16
|
job
|
17
17
|
execution_interrupted
|
18
|
-
|
18
|
+
retried_job
|
19
19
|
retry_now
|
20
20
|
].freeze
|
21
21
|
|
@@ -61,11 +61,11 @@ module GoodJob
|
|
61
61
|
# @return [Boolean, nil]
|
62
62
|
thread_mattr_accessor :execution_interrupted
|
63
63
|
|
64
|
-
# @!attribute [rw]
|
64
|
+
# @!attribute [rw] retried_job
|
65
65
|
# @!scope class
|
66
66
|
# Execution Retried
|
67
|
-
# @return [
|
68
|
-
thread_mattr_accessor :
|
67
|
+
# @return [GoodJob::Job, nil]
|
68
|
+
thread_mattr_accessor :retried_job
|
69
69
|
|
70
70
|
# @!attribute [rw] retry_now
|
71
71
|
# @!scope class
|
@@ -14,9 +14,10 @@ module GoodJob # :nodoc:
|
|
14
14
|
|
15
15
|
# Registers the current process.
|
16
16
|
def register_process
|
17
|
+
@advisory_lock_heartbeat = GoodJob.configuration.advisory_lock_heartbeat
|
17
18
|
GoodJob::Process.override_connection(connection) do
|
18
19
|
GoodJob::Process.cleanup
|
19
|
-
@capsule.tracker.register(with_advisory_lock:
|
20
|
+
@capsule.tracker.register(with_advisory_lock: @advisory_lock_heartbeat)
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
@@ -33,7 +34,7 @@ module GoodJob # :nodoc:
|
|
33
34
|
# Deregisters the current process.
|
34
35
|
def deregister_process
|
35
36
|
GoodJob::Process.override_connection(connection) do
|
36
|
-
@capsule.tracker.unregister(with_advisory_lock:
|
37
|
+
@capsule.tracker.unregister(with_advisory_lock: @advisory_lock_heartbeat)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|