good_job 4.0.3 → 4.1.1
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 +1 -1
- data/app/charts/good_job/base_chart.rb +25 -0
- data/app/charts/good_job/performance_index_chart.rb +69 -0
- data/app/charts/good_job/performance_show_chart.rb +71 -0
- data/app/charts/good_job/scheduled_by_queue_chart.rb +23 -28
- data/app/controllers/good_job/application_controller.rb +1 -1
- data/app/controllers/good_job/frontends_controller.rb +6 -2
- data/app/controllers/good_job/metrics_controller.rb +5 -15
- data/app/controllers/good_job/performance_controller.rb +6 -1
- data/app/frontend/good_job/icons.svg +79 -0
- data/app/frontend/good_job/modules/charts.js +5 -17
- data/app/frontend/good_job/style.css +5 -0
- data/app/helpers/good_job/application_helper.rb +9 -1
- 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 +14 -35
- data/app/models/concerns/good_job/reportable.rb +8 -12
- data/app/models/good_job/batch.rb +10 -5
- data/app/models/good_job/batch_record.rb +18 -15
- data/app/models/good_job/discrete_execution.rb +6 -60
- 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 +569 -14
- data/app/models/good_job/process.rb +13 -29
- data/app/views/good_job/batches/_jobs.erb +1 -1
- data/app/views/good_job/batches/_table.erb +1 -1
- data/app/views/good_job/jobs/index.html.erb +1 -1
- data/app/views/good_job/performance/index.html.erb +3 -1
- data/app/views/good_job/performance/show.html.erb +5 -0
- data/app/views/good_job/shared/_filter.erb +2 -2
- data/app/views/layouts/good_job/application.html.erb +7 -7
- data/config/brakeman.ignore +75 -0
- data/config/locales/de.yml +52 -48
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +16 -12
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/ja.yml +4 -0
- data/config/locales/ko.yml +4 -0
- data/config/locales/nl.yml +4 -0
- data/config/locales/pt-BR.yml +4 -0
- data/config/locales/ru.yml +4 -0
- data/config/locales/tr.yml +4 -0
- data/config/locales/uk.yml +4 -0
- data/config/routes.rb +4 -4
- data/lib/good_job/active_job_extensions/concurrency.rb +105 -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 +1 -1
- data/lib/good_job/configuration.rb +3 -4
- data/lib/good_job/cron_manager.rb +1 -3
- data/lib/good_job/current_thread.rb +4 -4
- data/lib/good_job/notifier.rb +7 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +6 -5
- metadata +10 -20
- data/app/models/good_job/base_execution.rb +0 -609
- 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/locales/pt-BR.yml
CHANGED
@@ -197,11 +197,15 @@ pt-BR:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Duração média
|
200
|
+
chart_title: Tempo total de execução do trabalho em segundos
|
200
201
|
executions: Execuções
|
201
202
|
job_class: Classe de trabalho
|
202
203
|
maximum_duration: Duração máxima
|
203
204
|
minimum_duration: Duração mínima
|
204
205
|
title: Desempenho
|
206
|
+
show:
|
207
|
+
slow: Lento
|
208
|
+
title: Desempenho
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Agendamento ativado
|
data/config/locales/ru.yml
CHANGED
@@ -223,11 +223,15 @@ ru:
|
|
223
223
|
performance:
|
224
224
|
index:
|
225
225
|
average_duration: Средняя продолжительность
|
226
|
+
chart_title: Общее время выполнения задания в секундах
|
226
227
|
executions: Казни
|
227
228
|
job_class: Класс работы
|
228
229
|
maximum_duration: Максимальная продолжительность
|
229
230
|
minimum_duration: Минимальная продолжительность
|
230
231
|
title: Производительность
|
232
|
+
show:
|
233
|
+
slow: Медленный
|
234
|
+
title: Производительность
|
231
235
|
processes:
|
232
236
|
index:
|
233
237
|
cron_enabled: Cron включен
|
data/config/locales/tr.yml
CHANGED
@@ -197,11 +197,15 @@ tr:
|
|
197
197
|
performance:
|
198
198
|
index:
|
199
199
|
average_duration: Ortalama süre
|
200
|
+
chart_title: Saniye cinsinden toplam iş yürütme süresi
|
200
201
|
executions: İnfazlar
|
201
202
|
job_class: İş sınıfı
|
202
203
|
maximum_duration: Maksimum süre
|
203
204
|
minimum_duration: Minimum süre
|
204
205
|
title: Verim
|
206
|
+
show:
|
207
|
+
slow: Yavaş
|
208
|
+
title: Verim
|
205
209
|
processes:
|
206
210
|
index:
|
207
211
|
cron_enabled: Cron etkin
|
data/config/locales/uk.yml
CHANGED
@@ -223,11 +223,15 @@ uk:
|
|
223
223
|
performance:
|
224
224
|
index:
|
225
225
|
average_duration: Середня тривалість
|
226
|
+
chart_title: Загальний час виконання завдання в секундах
|
226
227
|
executions: Страти
|
227
228
|
job_class: Клас роботи
|
228
229
|
maximum_duration: Максимальна тривалість
|
229
230
|
minimum_duration: Мінімальна тривалість
|
230
231
|
title: Продуктивність
|
232
|
+
show:
|
233
|
+
slow: Повільно
|
234
|
+
title: Продуктивність
|
231
235
|
processes:
|
232
236
|
index:
|
233
237
|
cron_enabled: Cron увімкнено
|
data/config/routes.rb
CHANGED
@@ -31,10 +31,10 @@ GoodJob::Engine.routes.draw do
|
|
31
31
|
|
32
32
|
resources :processes, only: %i[index]
|
33
33
|
|
34
|
-
resources :performance, only: %i[index]
|
34
|
+
resources :performance, only: %i[index show]
|
35
35
|
|
36
|
-
scope :frontend, controller: :frontends do
|
37
|
-
get "modules/:
|
38
|
-
get "static/:
|
36
|
+
scope :frontend, controller: :frontends, defaults: { version: GoodJob::VERSION.tr(".", "-") } do
|
37
|
+
get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: 'js' }
|
38
|
+
get "static/:version/:id", action: :static, as: :frontend_static, constraints: { format: %w[css js svg] }
|
39
39
|
end
|
40
40
|
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,43 @@ 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
|
+
exceeded = :limit unless allowed_active_job_ids.include?(job.job_id)
|
153
|
+
next
|
154
|
+
end
|
155
|
+
|
156
|
+
if throttle
|
157
|
+
throttle_limit = throttle[0]
|
158
|
+
throttle_period = throttle[1]
|
159
|
+
|
160
|
+
query = Execution.joins(:job)
|
161
|
+
.where(GoodJob::Job.table_name => { concurrency_key: key })
|
162
|
+
.where(Execution.arel_table[:created_at].gt(Execution.bind_value('created_at', throttle_period.ago, ActiveRecord::Type::DateTime)))
|
163
|
+
allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
|
164
|
+
.order(created_at: :asc)
|
165
|
+
.limit(throttle_limit)
|
166
|
+
.pluck(:active_job_id)
|
167
|
+
|
168
|
+
exceeded = :throttle unless allowed_active_job_ids.include?(job.job_id)
|
169
|
+
next
|
170
|
+
end
|
91
171
|
end
|
92
172
|
|
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)
|
173
|
+
raise ActiveRecord::Rollback
|
174
|
+
end
|
104
175
|
|
105
|
-
|
106
|
-
|
176
|
+
if exceeded == :limit
|
177
|
+
raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError
|
178
|
+
elsif exceeded == :throttle
|
179
|
+
raise GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError
|
107
180
|
end
|
108
181
|
end
|
109
182
|
end
|
@@ -139,72 +212,6 @@ module GoodJob
|
|
139
212
|
def _good_job_default_concurrency_key
|
140
213
|
self.class.name.to_s
|
141
214
|
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
215
|
end
|
209
216
|
end
|
210
217
|
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
|
@@ -84,7 +84,7 @@ module GoodJob # :nodoc:
|
|
84
84
|
if !advisory_locked? || !advisory_locked_connection?
|
85
85
|
@record.class.transaction do
|
86
86
|
@record.advisory_lock!
|
87
|
-
@record.update(lock_type:
|
87
|
+
@record.update(lock_type: :advisory)
|
88
88
|
end
|
89
89
|
@advisory_locked_connection = WeakRef.new(@record.class.connection)
|
90
90
|
end
|
@@ -150,11 +150,10 @@ module GoodJob
|
|
150
150
|
# poll (using this interval) for new queued jobs to execute.
|
151
151
|
# @return [Integer]
|
152
152
|
def poll_interval
|
153
|
-
interval =
|
153
|
+
interval =
|
154
154
|
options[:poll_interval] ||
|
155
|
-
|
156
|
-
|
157
|
-
)
|
155
|
+
rails_config[:poll_interval] ||
|
156
|
+
env['GOOD_JOB_POLL_INTERVAL']
|
158
157
|
|
159
158
|
if interval
|
160
159
|
interval.to_i
|
@@ -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
|
data/lib/good_job/notifier.rb
CHANGED
@@ -27,6 +27,8 @@ module GoodJob # :nodoc:
|
|
27
27
|
RECONNECT_INTERVAL = 5
|
28
28
|
# Number of consecutive connection errors before reporting an error
|
29
29
|
CONNECTION_ERRORS_REPORTING_THRESHOLD = 6
|
30
|
+
# Interval for emitting a noop SQL query to keep the connection alive
|
31
|
+
KEEPALIVE_INTERVAL = 10
|
30
32
|
|
31
33
|
# Connection errors that will wait {RECONNECT_INTERVAL} before reconnecting
|
32
34
|
CONNECTION_ERRORS = %w[
|
@@ -78,6 +80,7 @@ module GoodJob # :nodoc:
|
|
78
80
|
@enable_listening = enable_listening
|
79
81
|
@task = nil
|
80
82
|
@capsule = capsule
|
83
|
+
@last_keepalive_time = Time.current
|
81
84
|
|
82
85
|
start
|
83
86
|
self.class.instances << self
|
@@ -269,6 +272,10 @@ module GoodJob # :nodoc:
|
|
269
272
|
raw_connection.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
|
270
273
|
yield(channel, payload)
|
271
274
|
end
|
275
|
+
if Time.current - @last_keepalive_time >= KEEPALIVE_INTERVAL
|
276
|
+
raw_connection.async_exec("SELECT 1")
|
277
|
+
@last_keepalive_time = Time.current
|
278
|
+
end
|
272
279
|
elsif @enable_listening && raw_connection.respond_to?(:jdbc_connection)
|
273
280
|
raw_connection.execute_query("SELECT 1")
|
274
281
|
notifications = raw_connection.jdbc_connection.getNotifications
|