good_job 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/app/charts/good_job/performance_index_chart.rb +1 -1
  4. data/app/charts/good_job/performance_show_chart.rb +1 -1
  5. data/app/controllers/good_job/application_controller.rb +1 -1
  6. data/app/controllers/good_job/frontends_controller.rb +6 -2
  7. data/app/controllers/good_job/performance_controller.rb +1 -1
  8. data/app/frontend/good_job/icons.svg +79 -0
  9. data/app/frontend/good_job/style.css +5 -0
  10. data/app/helpers/good_job/icons_helper.rb +8 -5
  11. data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
  12. data/app/models/concerns/good_job/reportable.rb +8 -12
  13. data/app/models/good_job/batch.rb +10 -5
  14. data/app/models/good_job/batch_record.rb +18 -15
  15. data/app/models/good_job/discrete_execution.rb +6 -59
  16. data/app/models/good_job/execution.rb +59 -4
  17. data/app/models/good_job/execution_result.rb +6 -6
  18. data/app/models/good_job/job.rb +567 -12
  19. data/app/views/good_job/batches/_jobs.erb +1 -1
  20. data/app/views/good_job/batches/_table.erb +1 -1
  21. data/app/views/good_job/jobs/index.html.erb +1 -1
  22. data/app/views/layouts/good_job/application.html.erb +7 -7
  23. data/config/brakeman.ignore +75 -0
  24. data/config/locales/de.yml +49 -49
  25. data/config/locales/es.yml +14 -14
  26. data/config/routes.rb +3 -3
  27. data/lib/good_job/active_job_extensions/concurrency.rb +105 -98
  28. data/lib/good_job/adapter/inline_buffer.rb +73 -0
  29. data/lib/good_job/adapter.rb +59 -53
  30. data/lib/good_job/configuration.rb +3 -4
  31. data/lib/good_job/cron_manager.rb +1 -3
  32. data/lib/good_job/current_thread.rb +4 -4
  33. data/lib/good_job/version.rb +1 -1
  34. data/lib/good_job.rb +6 -5
  35. metadata +6 -20
  36. data/app/models/good_job/base_execution.rb +0 -605
  37. data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
  38. data/app/views/good_job/shared/icons/_check.html.erb +0 -5
  39. data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
  40. data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
  41. data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
  42. data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
  43. data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
  44. data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
  45. data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
  46. data/app/views/good_job/shared/icons/_info.html.erb +0 -4
  47. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
  48. data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
  49. data/app/views/good_job/shared/icons/_play.html.erb +0 -4
  50. data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
  51. data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
  52. data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
@@ -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
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.1.0'
5
+ VERSION = '4.1.1'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "good_job/version"
7
7
  require_relative "good_job/engine"
8
8
 
9
9
  require_relative "good_job/adapter"
10
+ require_relative "good_job/adapter/inline_buffer"
10
11
  require_relative "active_job/queue_adapters/good_job_adapter"
11
12
  require_relative "good_job/active_job_extensions/batches"
12
13
  require_relative "good_job/active_job_extensions/concurrency"
@@ -211,7 +212,7 @@ module GoodJob
211
212
  ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
212
213
  deleted_jobs_count = 0
213
214
  deleted_batches_count = 0
214
- deleted_discrete_executions_count = 0
215
+ deleted_executions_count = 0
215
216
 
216
217
  jobs_query = GoodJob::Job.finished_before(timestamp).order(finished_at: :asc).limit(in_batches_of)
217
218
  jobs_query = jobs_query.succeeded unless include_discarded
@@ -219,8 +220,8 @@ module GoodJob
219
220
  active_job_ids = jobs_query.pluck(:active_job_id)
220
221
  break if active_job_ids.empty?
221
222
 
222
- deleted_discrete_executions = GoodJob::DiscreteExecution.where(active_job_id: active_job_ids).delete_all
223
- deleted_discrete_executions_count += deleted_discrete_executions
223
+ deleted_executions = GoodJob::Execution.where(active_job_id: active_job_ids).delete_all
224
+ deleted_executions_count += deleted_executions
224
225
 
225
226
  deleted_jobs = GoodJob::Job.where(active_job_id: active_job_ids).delete_all
226
227
  deleted_jobs_count += deleted_jobs
@@ -236,10 +237,10 @@ module GoodJob
236
237
  end
237
238
 
238
239
  payload[:destroyed_batches_count] = deleted_batches_count
239
- payload[:destroyed_discrete_executions_count] = deleted_discrete_executions_count
240
+ payload[:destroyed_executions_count] = deleted_executions_count
240
241
  payload[:destroyed_jobs_count] = deleted_jobs_count
241
242
 
242
- destroyed_records_count = deleted_batches_count + deleted_discrete_executions_count + deleted_jobs_count
243
+ destroyed_records_count = deleted_batches_count + deleted_executions_count + deleted_jobs_count
243
244
  payload[:destroyed_records_count] = destroyed_records_count
244
245
 
245
246
  destroyed_records_count
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-16 00:00:00.000000000 Z
11
+ date: 2024-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -264,6 +264,7 @@ files:
264
264
  - app/filters/good_job/batches_filter.rb
265
265
  - app/filters/good_job/jobs_filter.rb
266
266
  - app/frontend/good_job/application.js
267
+ - app/frontend/good_job/icons.svg
267
268
  - app/frontend/good_job/modules/async_values_controller.js
268
269
  - app/frontend/good_job/modules/charts.js
269
270
  - app/frontend/good_job/modules/checkbox_toggle.js
@@ -286,7 +287,6 @@ files:
286
287
  - app/models/concerns/good_job/filterable.rb
287
288
  - app/models/concerns/good_job/reportable.rb
288
289
  - app/models/good_job/active_record_parent_class.rb
289
- - app/models/good_job/base_execution.rb
290
290
  - app/models/good_job/base_record.rb
291
291
  - app/models/good_job/batch.rb
292
292
  - app/models/good_job/batch_record.rb
@@ -319,24 +319,9 @@ files:
319
319
  - app/views/good_job/shared/_footer.erb
320
320
  - app/views/good_job/shared/_navbar.erb
321
321
  - app/views/good_job/shared/_secondary_navbar.erb
322
- - app/views/good_job/shared/icons/_arrow_clockwise.html.erb
323
- - app/views/good_job/shared/icons/_check.html.erb
324
- - app/views/good_job/shared/icons/_circle_half.html.erb
325
- - app/views/good_job/shared/icons/_clock.html.erb
326
- - app/views/good_job/shared/icons/_dash_circle.html.erb
327
- - app/views/good_job/shared/icons/_dots.html.erb
328
- - app/views/good_job/shared/icons/_eject.html.erb
329
- - app/views/good_job/shared/icons/_exclamation.html.erb
330
- - app/views/good_job/shared/icons/_globe.html.erb
331
- - app/views/good_job/shared/icons/_info.html.erb
332
- - app/views/good_job/shared/icons/_moon_stars_fill.html.erb
333
- - app/views/good_job/shared/icons/_pause.html.erb
334
- - app/views/good_job/shared/icons/_play.html.erb
335
- - app/views/good_job/shared/icons/_skip_forward.html.erb
336
- - app/views/good_job/shared/icons/_stop.html.erb
337
- - app/views/good_job/shared/icons/_sun_fill.html.erb
338
322
  - app/views/good_job/shared/icons/_trash.html.erb
339
323
  - app/views/layouts/good_job/application.html.erb
324
+ - config/brakeman.ignore
340
325
  - config/locales/de.yml
341
326
  - config/locales/en.yml
342
327
  - config/locales/es.yml
@@ -363,6 +348,7 @@ files:
363
348
  - lib/good_job/active_job_extensions/labels.rb
364
349
  - lib/good_job/active_job_extensions/notify_options.rb
365
350
  - lib/good_job/adapter.rb
351
+ - lib/good_job/adapter/inline_buffer.rb
366
352
  - lib/good_job/bulk.rb
367
353
  - lib/good_job/callable.rb
368
354
  - lib/good_job/capsule.rb
@@ -427,7 +413,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
427
413
  - !ruby/object:Gem::Version
428
414
  version: '0'
429
415
  requirements: []
430
- rubygems_version: 3.5.14
416
+ rubygems_version: 3.5.11
431
417
  signing_key:
432
418
  specification_version: 4
433
419
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails