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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +1 -1
  4. data/app/charts/good_job/base_chart.rb +25 -0
  5. data/app/charts/good_job/performance_index_chart.rb +69 -0
  6. data/app/charts/good_job/performance_show_chart.rb +71 -0
  7. data/app/charts/good_job/scheduled_by_queue_chart.rb +23 -28
  8. data/app/controllers/good_job/application_controller.rb +1 -1
  9. data/app/controllers/good_job/frontends_controller.rb +6 -2
  10. data/app/controllers/good_job/metrics_controller.rb +5 -15
  11. data/app/controllers/good_job/performance_controller.rb +6 -1
  12. data/app/frontend/good_job/icons.svg +79 -0
  13. data/app/frontend/good_job/modules/charts.js +5 -17
  14. data/app/frontend/good_job/style.css +5 -0
  15. data/app/helpers/good_job/application_helper.rb +9 -1
  16. data/app/helpers/good_job/icons_helper.rb +8 -5
  17. data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
  18. data/app/models/concerns/good_job/error_events.rb +14 -35
  19. data/app/models/concerns/good_job/reportable.rb +8 -12
  20. data/app/models/good_job/batch.rb +10 -5
  21. data/app/models/good_job/batch_record.rb +18 -15
  22. data/app/models/good_job/discrete_execution.rb +6 -60
  23. data/app/models/good_job/execution.rb +59 -4
  24. data/app/models/good_job/execution_result.rb +6 -6
  25. data/app/models/good_job/job.rb +569 -14
  26. data/app/models/good_job/process.rb +13 -29
  27. data/app/views/good_job/batches/_jobs.erb +1 -1
  28. data/app/views/good_job/batches/_table.erb +1 -1
  29. data/app/views/good_job/jobs/index.html.erb +1 -1
  30. data/app/views/good_job/performance/index.html.erb +3 -1
  31. data/app/views/good_job/performance/show.html.erb +5 -0
  32. data/app/views/good_job/shared/_filter.erb +2 -2
  33. data/app/views/layouts/good_job/application.html.erb +7 -7
  34. data/config/brakeman.ignore +75 -0
  35. data/config/locales/de.yml +52 -48
  36. data/config/locales/en.yml +4 -0
  37. data/config/locales/es.yml +16 -12
  38. data/config/locales/fr.yml +4 -0
  39. data/config/locales/it.yml +4 -0
  40. data/config/locales/ja.yml +4 -0
  41. data/config/locales/ko.yml +4 -0
  42. data/config/locales/nl.yml +4 -0
  43. data/config/locales/pt-BR.yml +4 -0
  44. data/config/locales/ru.yml +4 -0
  45. data/config/locales/tr.yml +4 -0
  46. data/config/locales/uk.yml +4 -0
  47. data/config/routes.rb +4 -4
  48. data/lib/good_job/active_job_extensions/concurrency.rb +105 -98
  49. data/lib/good_job/adapter/inline_buffer.rb +73 -0
  50. data/lib/good_job/adapter.rb +59 -53
  51. data/lib/good_job/capsule_tracker.rb +1 -1
  52. data/lib/good_job/configuration.rb +3 -4
  53. data/lib/good_job/cron_manager.rb +1 -3
  54. data/lib/good_job/current_thread.rb +4 -4
  55. data/lib/good_job/notifier.rb +7 -0
  56. data/lib/good_job/version.rb +1 -1
  57. data/lib/good_job.rb +6 -5
  58. metadata +10 -20
  59. data/app/models/good_job/base_execution.rb +0 -609
  60. data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
  61. data/app/views/good_job/shared/icons/_check.html.erb +0 -5
  62. data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
  63. data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
  64. data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
  65. data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
  66. data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
  67. data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
  68. data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
  69. data/app/views/good_job/shared/icons/_info.html.erb +0 -4
  70. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
  71. data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
  72. data/app/views/good_job/shared/icons/_play.html.erb +0 -4
  73. data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
  74. data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
  75. data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
@@ -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
@@ -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 включен
@@ -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
@@ -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/:name", action: :module, as: :frontend_module, constraints: { format: 'js' }
38
- get "static/:name", action: :static, as: :frontend_static, constraints: { format: %w[css js] }
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
- GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_lock") do
84
- if limit
85
- allowed_active_job_ids = GoodJob::Job.unfinished.where(concurrency_key: key)
86
- .advisory_locked
87
- .order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC"))
88
- .limit(limit).pluck(:active_job_id)
89
- # The current job has already been locked and will appear in the previous query
90
- raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include?(job.job_id)
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
- if throttle
94
- throttle_limit = throttle[0]
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
- raise ThrottleExceededError unless allowed_active_job_ids.include?(job.job_id)
106
- end
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
@@ -57,7 +57,7 @@ module GoodJob
57
57
 
58
58
  Rails.application.executor.wrap do
59
59
  current_time = Time.current
60
- executions = active_jobs.map do |active_job|
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
- inline_executions = []
68
+ inline_jobs = []
69
69
  GoodJob::Job.transaction(requires_new: true, joinable: false) do
70
- execution_attributes = executions.map(&:attributes)
71
- results = GoodJob::Job.insert_all(execution_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
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
- executions.each do |execution|
79
- execution.instance_variable_set(:@new_record, false) if job_id_to_provider_job_id[execution.active_job_id]
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
- executions = executions.select(&:persisted?) # prune unpersisted executions
81
+ jobs = jobs.select(&:persisted?) # prune unpersisted jobs
82
82
 
83
83
  if execute_inline?
84
- inline_executions = executions.select { |execution| (execution.scheduled_at.nil? || execution.scheduled_at <= Time.current) }
85
- inline_executions.each(&:advisory_lock!)
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
- @capsule.tracker.register
90
- begin
91
- until inline_executions.empty?
92
- begin
93
- inline_execution = inline_executions.shift
94
- inline_result = inline_execution.perform(lock_id: @capsule.tracker.id_for_lock)
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
- inline_execution.advisory_unlock
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
- non_inline_executions = executions.reject(&:finished_at)
114
- if non_inline_executions.any?
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
- non_inline_executions.group_by(&:queue_name).each do |queue_name, executions_by_queue|
117
- executions_by_queue.group_by(&:scheduled_at).each do |scheduled_at, executions_by_queue_and_scheduled_at|
118
- state = { queue_name: queue_name, count: executions_by_queue_and_scheduled_at.size }
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(*executions_by_queue_and_scheduled_at.map(&:active_job_id)).count { |active_job| send_notify?(active_job) }
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
- execution = GoodJob::Job.enqueue(
145
+ job = GoodJob::Job.enqueue(
152
146
  active_job,
153
147
  scheduled_at: scheduled_at
154
148
  )
155
149
  elsif will_execute_inline
156
- execution = GoodJob::Job.enqueue(
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
- begin
162
- result = @capsule.tracker.register { execution.perform(lock_id: @capsule.tracker.id_for_lock) }
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
- execution = GoodJob::Job.enqueue(
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(execution.job_state)
184
- Notifier.notify(execution.job_state) if !executed_locally && send_notify?(active_job)
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
- execution
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: GoodJob::Process::LOCK_TYPE_ADVISORY)
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
- rails_config[:poll_interval] ||
156
- env['GOOD_JOB_POLL_INTERVAL']
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.each do |_cron_key, task|
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
- execution_retried
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] execution_retried
64
+ # @!attribute [rw] retried_job
65
65
  # @!scope class
66
66
  # Execution Retried
67
- # @return [Boolean, nil]
68
- thread_mattr_accessor :execution_retried
67
+ # @return [GoodJob::Job, nil]
68
+ thread_mattr_accessor :retried_job
69
69
 
70
70
  # @!attribute [rw] retry_now
71
71
  # @!scope class
@@ -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