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 +4 -4
- data/CHANGELOG.md +16 -0
- data/app/models/good_job/base_execution.rb +14 -1
- data/app/models/good_job/discrete_execution.rb +52 -0
- data/app/models/good_job/execution.rb +118 -15
- data/app/models/good_job/job.rb +10 -2
- data/app/views/good_job/jobs/show.html.erb +30 -4
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +18 -0
- data/lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb +32 -0
- data/lib/good_job/adapter.rb +9 -5
- data/lib/good_job/dependencies.rb +5 -26
- data/lib/good_job/engine.rb +7 -12
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +15 -5
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee08a99ae7d26df57e3f0a7a64b737e987041815805117ac6d4e6119df229932
|
|
4
|
+
data.tar.gz: fd747fed38d2227bde7898438b6d597d55c5cb72f6f7125171b5637aff168d5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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]
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -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:
|
|
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"
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
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
|
data/lib/good_job/adapter.rb
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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?)
|
data/lib/good_job/engine.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
data/lib/good_job/version.rb
CHANGED
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
|
-
|
|
178
|
-
break if
|
|
178
|
+
active_job_ids = jobs_query.pluck(:active_job_id)
|
|
179
|
+
break if active_job_ids.empty?
|
|
179
180
|
|
|
180
|
-
|
|
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[:
|
|
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.
|
|
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-
|
|
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.
|
|
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
|