good_job 4.1.0 → 4.1.1

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