good_job 4.1.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|