good_job 3.15.2 → 3.15.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4674d611fd466270ee2bb79b4763848bced81ed16ae227dc05e0daca00b5672e
4
- data.tar.gz: 40dda31aa1e21e6b16e4f2582354d572af271ebe270b80e6cbfceec95873f5da
3
+ metadata.gz: ee08a99ae7d26df57e3f0a7a64b737e987041815805117ac6d4e6119df229932
4
+ data.tar.gz: fd747fed38d2227bde7898438b6d597d55c5cb72f6f7125171b5637aff168d5a
5
5
  SHA512:
6
- metadata.gz: 73ae44f8b88e70d4cfd98473920af572320157432f121b437f99718ca2182fa88d98b7f3dd4a3aec0319b6f2ddd51eb5eed1fe0290f94b605a4b9b7fedf53cc6
7
- data.tar.gz: 932c23ebe5722a3a8d7638db77bfeefd350c219a071b8b96982cd19ad78319c2ddd3f7b178d3ebb59b7d670caf7b265c01b254c2481d13a7a6f0521f0d172c82
6
+ metadata.gz: e32e66f000d6d7cd0a84fd6ab5a7bdf08f2a5f996bd5533ffc11f68ba33ee96e9d4ddc90a64dea1dbfcad43b07b75fcb34ed66c38546cda52b2cd5c0da90a329
7
+ data.tar.gz: 8f2a8c72500ee21f634ae6436d12ddc0641e730281b8b9534189f5892b795c6dc3a214e30e4a348878285a8912342dea3fbc74a6ab615a4de5b38f025767db3f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.15.4](https://github.com/bensheldon/good_job/tree/v3.15.4) (2023-04-22)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.3...v3.15.4)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Create "discrete" `good_job_executions` table to separate Job records from Execution records and have a 1-to-1 correspondence between `good_jobs` records and Active Job jobs [\#928](https://github.com/bensheldon/good_job/pull/928) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ ## [v3.15.3](https://github.com/bensheldon/good_job/tree/v3.15.3) (2023-04-22)
12
+
13
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.2...v3.15.3)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Eagerly autoload `ActiveJob::Base`; clean up framework deferred-loading logic to use nested `on_load` blocks [\#931](https://github.com/bensheldon/good_job/pull/931) ([bensheldon](https://github.com/bensheldon))
18
+
3
19
  ## [v3.15.2](https://github.com/bensheldon/good_job/tree/v3.15.2) (2023-04-19)
4
20
 
5
21
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.1...v3.15.2)
@@ -33,12 +33,25 @@ module GoodJob
33
33
  def coalesce_scheduled_at_created_at
34
34
  arel_table.coalesce(arel_table['scheduled_at'], arel_table['created_at'])
35
35
  end
36
+
37
+ def discrete_support?
38
+ if connection.table_exists?('good_job_executions')
39
+ true
40
+ else
41
+ migration_pending_warning!
42
+ false
43
+ end
44
+ end
36
45
  end
37
46
 
38
47
  # The ActiveJob job class, as a string
39
48
  # @return [String]
40
49
  def job_class
41
- serialized_params['job_class']
50
+ discrete? ? attributes['job_class'] : serialized_params['job_class']
51
+ end
52
+
53
+ def discrete?
54
+ self.class.discrete_support? && is_discrete?
42
55
  end
43
56
  end
44
57
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob # :nodoc:
4
+ class DiscreteExecution < BaseRecord
5
+ self.table_name = 'good_job_executions'
6
+
7
+ belongs_to :execution, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
8
+ belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
9
+
10
+ scope :finished, -> { where.not(finished_at: nil) }
11
+
12
+ alias_attribute :performed_at, :created_at
13
+
14
+ def number
15
+ serialized_params.fetch('executions', 0) + 1
16
+ end
17
+
18
+ # Time between when this job was expected to run and when it started running
19
+ def queue_latency
20
+ created_at - scheduled_at
21
+ end
22
+
23
+ # Time between when this job started and finished
24
+ def runtime_latency
25
+ (finished_at || Time.current) - performed_at if performed_at
26
+ end
27
+
28
+ def last_status_at
29
+ finished_at || created_at
30
+ end
31
+
32
+ def status
33
+ if finished_at.present?
34
+ if error.present?
35
+ :retried
36
+ elsif error.present? && job.finished_at.present?
37
+ :discarded
38
+ else
39
+ :succeeded
40
+ end
41
+ else
42
+ :running
43
+ end
44
+ end
45
+
46
+ def display_serialized_params
47
+ serialized_params.merge({
48
+ _good_job_execution: attributes.except('serialized_params'),
49
+ })
50
+ end
51
+ end
52
+ end
@@ -70,9 +70,13 @@ module GoodJob
70
70
  end
71
71
 
72
72
  belongs_to :batch, class_name: 'GoodJob::BatchRecord', optional: true, inverse_of: :executions
73
-
74
73
  belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
75
- after_destroy -> { self.class.active_job_id(active_job_id).delete_all }, if: -> { @_destroy_job }
74
+ has_many :discrete_executions, class_name: 'GoodJob::DiscreteExecution', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :execution # rubocop:disable Rails/HasManyOrHasOneDependent
75
+
76
+ after_destroy lambda {
77
+ GoodJob::DiscreteExecution.where(active_job_id: active_job_id).delete_all if discrete? # TODO: move into association `dependent: :delete_all` after v4
78
+ self.class.active_job_id(active_job_id).delete_all
79
+ }, if: -> { @_destroy_job }
76
80
 
77
81
  # Get executions with given ActiveJob ID
78
82
  # @!method active_job_id
@@ -201,8 +205,12 @@ module GoodJob
201
205
  end
202
206
  end)
203
207
 
204
- # Construct a GoodJob::Execution from an ActiveJob instance.
205
208
  def self.build_for_enqueue(active_job, overrides = {})
209
+ new(**enqueue_args(active_job, overrides))
210
+ end
211
+
212
+ # Construct arguments for GoodJob::Execution from an ActiveJob instance.
213
+ def self.enqueue_args(active_job, overrides = {})
206
214
  if active_job.priority && GoodJob.configuration.smaller_number_is_higher_priority.nil?
207
215
  ActiveSupport::Deprecation.warn(<<~DEPRECATION)
208
216
  The next major version of GoodJob (v4.0) will change job `priority` to give smaller numbers higher priority (default: `0`), in accordance with Active Job's definition of priority.
@@ -218,6 +226,7 @@ module GoodJob
218
226
  serialized_params: active_job.serialize,
219
227
  scheduled_at: active_job.scheduled_at,
220
228
  }
229
+
221
230
  execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
222
231
 
223
232
  reenqueued_current_execution = CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
@@ -238,7 +247,7 @@ module GoodJob
238
247
  execution_args[:cron_at] = CurrentThread.cron_at
239
248
  end
240
249
 
241
- new(**execution_args.merge(overrides))
250
+ execution_args.merge(overrides)
242
251
  end
243
252
 
244
253
  # Finds the next eligible Execution, acquire an advisory lock related to it, and
@@ -298,19 +307,47 @@ module GoodJob
298
307
  # The new {Execution} instance representing the queued ActiveJob job.
299
308
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
300
309
  ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
301
- execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
310
+ current_execution = CurrentThread.execution
311
+
312
+ retried = current_execution && current_execution.active_job_id == active_job.job_id
313
+ if retried
314
+ if current_execution.discrete?
315
+ execution = current_execution
316
+ execution.assign_attributes(enqueue_args(active_job, { scheduled_at: scheduled_at }))
317
+ execution.scheduled_at ||= Time.current
318
+ execution.performed_at = nil
319
+ execution.finished_at = nil
320
+ else
321
+ execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
322
+ end
323
+ else
324
+ execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
325
+ execution.make_discrete if discrete_support?
326
+ end
302
327
 
303
- execution.create_with_advisory_lock = create_with_advisory_lock
304
- instrument_payload[:execution] = execution
328
+ if create_with_advisory_lock
329
+ if execution.persisted?
330
+ execution.advisory_lock
331
+ else
332
+ execution.create_with_advisory_lock = true
333
+ end
334
+ end
305
335
 
336
+ instrument_payload[:execution] = execution
306
337
  execution.save!
307
- active_job.provider_job_id = execution.id
308
- CurrentThread.execution.retried_good_job_id = execution.id if CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
309
338
 
339
+ CurrentThread.execution.retried_good_job_id = execution.id if retried && !CurrentThread.execution.discrete?
340
+ active_job.provider_job_id = execution.id
310
341
  execution
311
342
  end
312
343
  end
313
344
 
345
+ def self.format_error(error)
346
+ raise ArgumentError unless error.is_a?(Exception)
347
+
348
+ [error.class.to_s, ERROR_MESSAGE_SEPARATOR, error.message].join
349
+ end
350
+
314
351
  # Execute the ActiveJob job this {Execution} represents.
315
352
  # @return [ExecutionResult]
316
353
  # An array of the return value of the job's +#perform+ method and the
@@ -320,12 +357,39 @@ module GoodJob
320
357
  run_callbacks(:perform) do
321
358
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
322
359
 
360
+ discrete_execution = nil
323
361
  result = GoodJob::CurrentThread.within do |current_thread|
324
362
  current_thread.reset
325
363
  current_thread.execution = self
326
364
 
327
- current_thread.execution_interrupted = performed_at if performed_at
328
- update!(performed_at: Time.current)
365
+ if performed_at
366
+ current_thread.execution_interrupted = performed_at
367
+
368
+ if discrete?
369
+ interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{performed_at}'"))
370
+ self.error = interrupt_error_string
371
+ discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all( # rubocop:disable Rails/SkipsModelValidations
372
+ error: interrupt_error_string,
373
+ finished_at: Time.current
374
+ )
375
+ end
376
+ end
377
+
378
+ if discrete?
379
+ transaction do
380
+ now = Time.current
381
+ discrete_execution = discrete_executions.create!(
382
+ job_class: job_class,
383
+ queue_name: queue_name,
384
+ serialized_params: serialized_params,
385
+ scheduled_at: (scheduled_at || created_at),
386
+ created_at: now
387
+ )
388
+ update!(performed_at: now, executions_count: ((executions_count || 0) + 1))
389
+ end
390
+ else
391
+ update!(performed_at: Time.current)
392
+ end
329
393
 
330
394
  ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do |instrument_payload|
331
395
  value = ActiveJob::Base.execute(active_job_data)
@@ -349,14 +413,42 @@ module GoodJob
349
413
  end
350
414
 
351
415
  job_error = result.handled_error || result.unhandled_error
352
- self.error = [job_error.class, ERROR_MESSAGE_SEPARATOR, job_error.message].join if job_error
416
+
417
+ if job_error
418
+ error_string = self.class.format_error(job_error)
419
+ self.error = error_string
420
+ discrete_execution.error = error_string if discrete_execution
421
+ else
422
+ self.error = nil
423
+ end
353
424
 
354
425
  reenqueued = result.retried? || retried_good_job_id.present?
355
426
  if result.unhandled_error && GoodJob.retry_on_unhandled_error
356
- save!
427
+ if discrete_execution
428
+ transaction do
429
+ discrete_execution.update!(finished_at: Time.current)
430
+ update!(performed_at: nil, finished_at: nil, retried_good_job_id: nil)
431
+ end
432
+ else
433
+ save!
434
+ end
357
435
  elsif GoodJob.preserve_job_records == true || reenqueued || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error) || cron_key.present?
358
- self.finished_at = Time.current
359
- save!
436
+ now = Time.current
437
+ if discrete_execution
438
+ if reenqueued
439
+ self.performed_at = nil
440
+ else
441
+ self.finished_at = now
442
+ end
443
+ discrete_execution.finished_at = now
444
+ transaction do
445
+ discrete_execution.save!
446
+ save!
447
+ end
448
+ else
449
+ self.finished_at = now
450
+ save!
451
+ end
360
452
  else
361
453
  destroy_job
362
454
  end
@@ -371,6 +463,17 @@ module GoodJob
371
463
  self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
372
464
  end
373
465
 
466
+ def make_discrete
467
+ self.is_discrete = true
468
+ self.id = active_job_id
469
+ self.job_class = serialized_params['job_class']
470
+ self.executions_count ||= 0
471
+
472
+ current_time = Time.current
473
+ self.created_at ||= current_time
474
+ self.scheduled_at ||= current_time
475
+ end
476
+
374
477
  # Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
375
478
  #
376
479
  # @param ignore_deserialization_errors [Boolean]
@@ -30,6 +30,11 @@ module GoodJob
30
30
 
31
31
  belongs_to :batch, class_name: 'GoodJob::BatchRecord', inverse_of: :jobs, optional: true
32
32
  has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
33
+ has_many :discrete_executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::DiscreteExecution', foreign_key: 'active_job_id', primary_key: :active_job_id, inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
34
+
35
+ after_destroy lambda {
36
+ GoodJob::DiscreteExecution.where(active_job_id: active_job_id).delete_all if discrete? # TODO: move into association `dependent: :delete_all` after v4
37
+ }
33
38
 
34
39
  # Only the most-recent unretried execution represents a "Job"
35
40
  default_scope { where(retried_good_job_id: nil) }
@@ -56,6 +61,8 @@ module GoodJob
56
61
  # Errored but will not be retried
57
62
  scope :discarded, -> { finished.where.not(error: nil) }
58
63
 
64
+ scope :unfinished_undiscrete, -> { where(finished_at: nil, retried_good_job_id: nil, is_discrete: [nil, false]) }
65
+
59
66
  # The job's ActiveJob UUID
60
67
  # @return [String]
61
68
  def id
@@ -191,9 +198,10 @@ module GoodJob
191
198
 
192
199
  execution.class.transaction(joinable: false, requires_new: true) do
193
200
  new_active_job = active_job.retry_job(wait: 0, error: execution.error)
194
- execution.save
201
+ execution.save!
195
202
  end
196
203
  end
204
+
197
205
  new_active_job
198
206
  end
199
207
  end
@@ -213,7 +221,7 @@ module GoodJob
213
221
  update_execution = proc do
214
222
  execution.update(
215
223
  finished_at: Time.current,
216
- error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
224
+ error: GoodJob::Execution.format_error(job_error)
217
225
  )
218
226
  end
219
227
 
@@ -3,7 +3,12 @@
3
3
  <nav aria-label="breadcrumb">
4
4
  <ol class="breadcrumb small mb-0">
5
5
  <li class="breadcrumb-item"><%= link_to t(".jobs"), jobs_path %></li>
6
- <li class="breadcrumb-item active" aria-current="page"><%= tag.code @job.id, class: "text-muted" %></li>
6
+ <li class="breadcrumb-item active" aria-current="page">
7
+ <%= tag.code @job.id, class: "text-muted" %>
8
+ <% if @job.discrete? %>
9
+ <span class="badge bg-info text-dark">Discrete</span>
10
+ <% end %>
11
+ </li>
7
12
  </ol>
8
13
  </nav>
9
14
  <div class="row align-items-center">
@@ -21,6 +26,10 @@
21
26
  <div class="font-monospace fw-bold small my-2"><%= tag.strong @job.priority %></div>
22
27
  </div>
23
28
  <div class="col text-end">
29
+ <div class="mb-2">
30
+ <%= tag.span relative_time(@job.last_status_at), class: "small" %>
31
+ <%= status_badge @job.status %>
32
+ </div>
24
33
  <% if @job.status.in? [:scheduled, :retried, :queued] %>
25
34
  <%= button_to reschedule_job_path(@job.id), method: :put,
26
35
  class: "btn btn-sm btn-outline-primary",
@@ -59,8 +68,25 @@
59
68
  </div>
60
69
 
61
70
  <div class="my-4">
62
- <h5><%= t "good_job.models.job.arguments" %></h5>
63
- <%= tag.pre @job.serialized_params["arguments"].map(&:inspect).join(', '), class: 'text-wrap text-break' %>
71
+ <h5>
72
+ <%= t "good_job.models.job.arguments" %>
73
+ <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
74
+ title: t("good_job.actions.inspect"),
75
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(@job, 'params')}" },
76
+ aria: { expanded: false, controls: dom_id(@job, "params") } do %>
77
+ <%= render_icon "info" %>
78
+ <span class="visually-hidden"><%= t "good_job.actions.inspect" %></span>
79
+ <% end %>
80
+ </h5>
64
81
  </div>
82
+ <%= tag.pre @job.serialized_params["arguments"].map(&:inspect).join(', '), class: 'text-wrap text-break' %>
83
+
84
+ <%= tag.div id: dom_id(@job, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
85
+ <%= tag.pre JSON.pretty_generate(@job.display_serialized_params) %>
86
+ <% end %>
65
87
 
66
- <%= render 'executions', executions: @job.executions.includes_advisory_locks.reverse %>
88
+ <% if @job.discrete? %>
89
+ <%= render 'executions', executions: @job.discrete_executions.reverse %>
90
+ <% else %>
91
+ <%= render 'executions', executions: @job.executions.includes_advisory_locks.reverse %>
92
+ <% end %>
@@ -22,6 +22,10 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
22
22
 
23
23
  t.uuid :batch_id
24
24
  t.uuid :batch_callback_id
25
+
26
+ t.boolean :is_discrete
27
+ t.integer :executions_count
28
+ t.text :job_class
25
29
  end
26
30
 
27
31
  create_table :good_job_batches, id: :uuid do |t|
@@ -38,6 +42,18 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
38
42
  t.datetime :finished_at
39
43
  end
40
44
 
45
+ create_table :good_job_executions, id: :uuid do |t|
46
+ t.timestamps
47
+
48
+ t.uuid :active_job_id, null: false
49
+ t.text :job_class
50
+ t.text :queue_name
51
+ t.jsonb :serialized_params
52
+ t.datetime :scheduled_at
53
+ t.datetime :finished_at
54
+ t.text :error
55
+ end
56
+
41
57
  create_table :good_job_processes, id: :uuid do |t|
42
58
  t.timestamps
43
59
  t.jsonb :state
@@ -62,5 +78,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
62
78
  where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished
63
79
  add_index :good_jobs, [:batch_id], where: "batch_id IS NOT NULL"
64
80
  add_index :good_jobs, [:batch_callback_id], where: "batch_callback_id IS NOT NULL"
81
+
82
+ add_index :good_job_executions, [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
65
83
  end
66
84
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ class CreateGoodJobExecutions < ActiveRecord::Migration<%= migration_version %>
3
+ def change
4
+ reversible do |dir|
5
+ dir.up do
6
+ # Ensure this incremental update migration is idempotent
7
+ # with monolithic install migration.
8
+ return if connection.table_exists?(:good_job_executions)
9
+ end
10
+ end
11
+
12
+ create_table :good_job_executions, id: :uuid do |t|
13
+ t.timestamps
14
+
15
+ t.uuid :active_job_id, null: false
16
+ t.text :job_class
17
+ t.text :queue_name
18
+ t.jsonb :serialized_params
19
+ t.datetime :scheduled_at
20
+ t.datetime :finished_at
21
+ t.text :error
22
+
23
+ t.index [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
24
+ end
25
+
26
+ change_table :good_jobs do |t|
27
+ t.boolean :is_discrete
28
+ t.integer :executions_count
29
+ t.text :job_class
30
+ end
31
+ end
32
+ end
@@ -50,11 +50,15 @@ module GoodJob
50
50
 
51
51
  current_time = Time.current
52
52
  executions = active_jobs.map do |active_job|
53
- GoodJob::Execution.build_for_enqueue(active_job, {
54
- id: SecureRandom.uuid,
55
- created_at: current_time,
56
- updated_at: current_time,
57
- })
53
+ GoodJob::Execution.build_for_enqueue(active_job).tap do |execution|
54
+ if GoodJob::Execution.discrete_support?
55
+ execution.make_discrete
56
+ execution.scheduled_at = current_time if execution.scheduled_at == execution.created_at
57
+ end
58
+
59
+ execution.created_at = current_time
60
+ execution.updated_at = current_time
61
+ end
58
62
  end
59
63
 
60
64
  inline_executions = []
@@ -6,40 +6,19 @@ module GoodJob # :nodoc:
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- # @!attribute [rw] _rails_after_initialize_hook_called
10
- # @!scope class
11
- # Whether Railtie.after_initialize has been called yet (default: +false+).
12
- # This will be set on but before +Rails.application.initialize?+ is +true+.
13
- # @return [Boolean]
14
- mattr_accessor :_rails_after_initialize_hook_called, default: false
15
-
16
- # @!attribute [rw] _active_job_loaded
17
- # @!scope class
18
- # Whether ActiveJob has loaded (default: +false+).
19
- # @return [Boolean]
20
- mattr_accessor :_active_job_loaded, default: false
21
-
22
- # @!attribute [rw] _active_record_loaded
23
- # @!scope class
24
- # Whether ActiveRecord has loaded (default: +false+).
25
- # @return [Boolean]
26
- mattr_accessor :_active_record_loaded, default: false
9
+ mattr_accessor :_framework_ready, default: false
27
10
  end
28
11
 
29
12
  class_methods do
30
- # Whether GoodJob's has been initialized as of the calling of +Railtie.after_initialize+.
31
- # @return [Boolean]
13
+ # Whether Rails framework has sufficiently initialized to enable Async execution.
32
14
  def async_ready?
33
- Rails.application.initialized? || (
34
- _rails_after_initialize_hook_called &&
35
- _active_job_loaded &&
36
- _active_record_loaded
37
- )
15
+ Rails.application.initialized? || _framework_ready
38
16
  end
39
17
 
40
- def start_async_adapters
18
+ def _start_async_adapters
41
19
  return unless async_ready?
42
20
 
21
+ ActiveJob::Base.queue_adapter # Ensure Active Job is initialized
43
22
  GoodJob::Adapter.instances
44
23
  .select(&:execute_async?)
45
24
  .reject(&:async_started?)
@@ -37,26 +37,21 @@ module GoodJob
37
37
 
38
38
  initializer "good_job.start_async" do
39
39
  # This hooks into the hookable places during Rails boot, which is unfortunately not Rails.application.initialized?
40
- # If an Adapter is initialized during boot, we want to want to start its async executors once the framework dependencies have loaded.
40
+ # If an Adapter is initialized during boot, we want to want to start async executors once the framework dependencies have loaded.
41
41
  # When exactly that happens is out of our control because gems or application code may touch things earlier than expected.
42
42
  # For example, as of Rails 6.1, if an ActiveRecord model is touched during boot, that triggers ActiveRecord to load,
43
43
  # which touches DestroyAssociationAsyncJob, which loads ActiveJob, which may initialize a GoodJob::Adapter, all of which
44
44
  # happens _before_ ActiveRecord finishes loading. GoodJob will deadlock if an async executor is started in the middle of
45
45
  # ActiveRecord loading.
46
-
47
46
  config.after_initialize do
48
47
  ActiveSupport.on_load(:active_record) do
49
- GoodJob._active_record_loaded = true
50
- GoodJob.start_async_adapters
51
- end
52
-
53
- ActiveSupport.on_load(:active_job) do
54
- GoodJob._active_job_loaded = true
55
- GoodJob.start_async_adapters
48
+ ActiveSupport.on_load(:active_job) do
49
+ GoodJob._framework_ready = true
50
+ GoodJob._start_async_adapters
51
+ end
52
+ GoodJob._start_async_adapters
56
53
  end
57
-
58
- GoodJob._rails_after_initialize_hook_called = true
59
- GoodJob.start_async_adapters
54
+ GoodJob._start_async_adapters
60
55
  end
61
56
  end
62
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '3.15.2'
4
+ VERSION = '3.15.4'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -170,14 +170,19 @@ module GoodJob
170
170
  ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
171
171
  deleted_executions_count = 0
172
172
  deleted_batches_count = 0
173
+ deleted_discrete_executions_count = 0
173
174
 
174
175
  jobs_query = GoodJob::Job.where('finished_at <= ?', timestamp).order(finished_at: :asc).limit(in_batches_of)
175
176
  jobs_query = jobs_query.succeeded unless include_discarded
176
177
  loop do
177
- deleted = GoodJob::Execution.where(job: jobs_query).delete_all
178
- break if deleted.zero?
178
+ active_job_ids = jobs_query.pluck(:active_job_id)
179
+ break if active_job_ids.empty?
179
180
 
180
- deleted_executions_count += deleted
181
+ deleted_discrete_executions = GoodJob::DiscreteExecution.where(active_job_id: active_job_ids).delete_all
182
+ deleted_discrete_executions_count += deleted_discrete_executions
183
+
184
+ deleted_executions = GoodJob::Execution.where(active_job_id: active_job_ids).delete_all
185
+ deleted_executions_count += deleted_executions
181
186
  end
182
187
 
183
188
  if GoodJob::BatchRecord.migrated?
@@ -191,9 +196,14 @@ module GoodJob
191
196
  end
192
197
  end
193
198
 
194
- payload[:destroyed_executions_count] = deleted_executions_count
195
199
  payload[:destroyed_batches_count] = deleted_batches_count
196
- payload[:destroyed_records_count] = deleted_executions_count + deleted_batches_count
200
+ payload[:destroyed_discrete_executions_count] = deleted_discrete_executions_count
201
+ payload[:destroyed_executions_count] = deleted_executions_count
202
+
203
+ destroyed_records_count = deleted_batches_count + deleted_discrete_executions_count + deleted_executions_count
204
+ payload[:destroyed_records_count] = destroyed_records_count
205
+
206
+ destroyed_records_count
197
207
  end
198
208
  end
199
209
 
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: 3.15.2
4
+ version: 3.15.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-19 00:00:00.000000000 Z
11
+ date: 2023-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -350,6 +350,7 @@ files:
350
350
  - app/models/good_job/batch.rb
351
351
  - app/models/good_job/batch_record.rb
352
352
  - app/models/good_job/cron_entry.rb
353
+ - app/models/good_job/discrete_execution.rb
353
354
  - app/models/good_job/execution.rb
354
355
  - app/models/good_job/execution_result.rb
355
356
  - app/models/good_job/job.rb
@@ -401,6 +402,7 @@ files:
401
402
  - lib/generators/good_job/templates/update/migrations/02_create_good_job_settings.rb.erb
402
403
  - lib/generators/good_job/templates/update/migrations/03_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb.erb
403
404
  - lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb
405
+ - lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb
404
406
  - lib/generators/good_job/update_generator.rb
405
407
  - lib/good_job.rb
406
408
  - lib/good_job/active_job_extensions/batches.rb
@@ -461,7 +463,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
461
463
  - !ruby/object:Gem::Version
462
464
  version: '0'
463
465
  requirements: []
464
- rubygems_version: 3.4.6
466
+ rubygems_version: 3.4.10
465
467
  signing_key:
466
468
  specification_version: 4
467
469
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails