good_job 3.15.0 → 3.16.3
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 +270 -0
- data/README.md +33 -13
- data/app/controllers/good_job/application_controller.rb +19 -10
- data/app/controllers/good_job/batches_controller.rb +1 -0
- data/app/controllers/good_job/cron_entries_controller.rb +2 -1
- data/app/controllers/good_job/frontends_controller.rb +1 -0
- data/app/controllers/good_job/jobs_controller.rb +1 -0
- data/app/controllers/good_job/processes_controller.rb +1 -0
- data/app/filters/good_job/base_filter.rb +1 -0
- data/app/filters/good_job/batches_filter.rb +1 -0
- data/app/filters/good_job/jobs_filter.rb +1 -0
- data/app/helpers/good_job/application_helper.rb +1 -1
- data/app/models/concerns/good_job/error_events.rb +45 -0
- data/app/models/concerns/good_job/filterable.rb +1 -0
- data/app/models/{good_job → concerns/good_job}/lockable.rb +3 -0
- data/app/models/concerns/good_job/reportable.rb +1 -0
- data/app/models/good_job/active_record_parent_class.rb +9 -0
- data/app/models/good_job/base_execution.rb +21 -1
- data/app/models/good_job/base_record.rb +6 -3
- data/app/models/good_job/cron_entry.rb +1 -0
- data/app/models/good_job/discrete_execution.rb +61 -0
- data/app/models/good_job/execution.rb +162 -25
- data/app/models/good_job/execution_result.rb +17 -2
- data/app/models/good_job/i18n_config.rb +25 -0
- data/app/models/good_job/job.rb +23 -7
- data/app/models/good_job/process.rb +66 -14
- data/app/views/good_job/batches/_jobs.erb +1 -1
- data/app/views/good_job/jobs/_executions.erb +1 -1
- data/app/views/good_job/jobs/_table.erb +18 -5
- data/app/views/good_job/jobs/show.html.erb +31 -5
- data/app/views/good_job/processes/index.html.erb +6 -2
- data/app/views/good_job/shared/_navbar.erb +1 -1
- data/config/locales/de.yml +9 -1
- data/config/locales/en.yml +9 -1
- data/config/locales/es.yml +9 -1
- data/config/locales/fr.yml +71 -63
- data/config/locales/ja.yml +227 -0
- data/config/locales/nl.yml +11 -3
- data/config/locales/ru.yml +9 -1
- data/config/locales/tr.yml +227 -0
- data/config/locales/{ua.yml → uk.yml} +34 -2
- data/config/routes.rb +1 -0
- data/exe/good_job +1 -0
- data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
- data/lib/generators/good_job/install_generator.rb +1 -0
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +23 -1
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +3 -1
- data/lib/generators/good_job/templates/update/migrations/02_create_good_job_settings.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/03_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb +33 -0
- data/lib/generators/good_job/templates/update/migrations/06_create_good_jobs_error_event.rb.erb +16 -0
- data/lib/generators/good_job/update_generator.rb +1 -0
- data/lib/good_job/active_job_extensions/batches.rb +1 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +2 -1
- data/lib/good_job/active_job_extensions/interrupt_errors.rb +1 -0
- data/lib/good_job/active_job_extensions/notify_options.rb +1 -0
- data/lib/good_job/adapter.rb +22 -8
- data/lib/good_job/assignable_connection.rb +1 -0
- data/lib/good_job/bulk.rb +1 -0
- data/lib/good_job/capsule.rb +22 -14
- data/lib/good_job/cleanup_tracker.rb +2 -1
- data/lib/good_job/cli.rb +2 -1
- data/lib/good_job/configuration.rb +13 -6
- data/lib/good_job/cron_manager.rb +3 -3
- data/lib/good_job/current_thread.rb +15 -0
- data/lib/good_job/daemon.rb +1 -0
- data/lib/good_job/dependencies.rb +5 -26
- data/lib/good_job/engine.rb +20 -12
- data/lib/good_job/interrupt_error.rb +1 -0
- data/lib/good_job/job_performer.rb +1 -0
- data/lib/good_job/log_subscriber.rb +1 -0
- data/lib/good_job/metrics.rb +57 -0
- data/lib/good_job/multi_scheduler.rb +1 -0
- data/lib/good_job/notifier/{process_registration.rb → process_heartbeat.rb} +12 -1
- data/lib/good_job/notifier.rb +18 -16
- data/lib/good_job/poller.rb +3 -3
- data/lib/good_job/scheduler.rb +30 -11
- data/lib/good_job/version.rb +5 -1
- data/lib/good_job.rb +67 -12
- metadata +15 -6
|
@@ -4,6 +4,11 @@ module GoodJob
|
|
|
4
4
|
# ActiveRecord model to share behavior between {Job} and {Execution} models
|
|
5
5
|
# which both read out of the same table.
|
|
6
6
|
class BaseExecution < BaseRecord
|
|
7
|
+
include ErrorEvents
|
|
8
|
+
include Filterable
|
|
9
|
+
include Lockable
|
|
10
|
+
include Reportable
|
|
11
|
+
|
|
7
12
|
self.table_name = 'good_jobs'
|
|
8
13
|
|
|
9
14
|
# With a given class name
|
|
@@ -33,12 +38,27 @@ module GoodJob
|
|
|
33
38
|
def coalesce_scheduled_at_created_at
|
|
34
39
|
arel_table.coalesce(arel_table['scheduled_at'], arel_table['created_at'])
|
|
35
40
|
end
|
|
41
|
+
|
|
42
|
+
def discrete_support?
|
|
43
|
+
GoodJob::DiscreteExecution.migrated?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def error_event_migrated?
|
|
47
|
+
return true if columns_hash["error_event"].present?
|
|
48
|
+
|
|
49
|
+
migration_pending_warning!
|
|
50
|
+
false
|
|
51
|
+
end
|
|
36
52
|
end
|
|
37
53
|
|
|
38
54
|
# The ActiveJob job class, as a string
|
|
39
55
|
# @return [String]
|
|
40
56
|
def job_class
|
|
41
|
-
serialized_params['job_class']
|
|
57
|
+
discrete? ? attributes['job_class'] : serialized_params['job_class']
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def discrete?
|
|
61
|
+
self.class.discrete_support? && is_discrete?
|
|
42
62
|
end
|
|
43
63
|
end
|
|
44
64
|
end
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module GoodJob
|
|
3
4
|
# Base ActiveRecord class that all GoodJob models inherit from.
|
|
4
5
|
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
|
5
6
|
# @!parse
|
|
6
7
|
# class BaseRecord < ActiveRecord::Base; end
|
|
7
|
-
class BaseRecord <
|
|
8
|
+
class BaseRecord < ActiveRecordParentClass
|
|
8
9
|
self.abstract_class = true
|
|
9
10
|
|
|
10
11
|
def self.migration_pending_warning!
|
|
11
|
-
|
|
12
|
+
GoodJob.deprecator.warn(<<~DEPRECATION)
|
|
12
13
|
GoodJob has pending database migrations. To create the migration files, run:
|
|
13
14
|
rails generate good_job:update
|
|
14
15
|
To apply the migration files, run:
|
|
@@ -21,10 +22,12 @@ module GoodJob
|
|
|
21
22
|
# Can be overriden by child class.
|
|
22
23
|
# @return [Boolean]
|
|
23
24
|
def self.migrated?
|
|
24
|
-
return true if
|
|
25
|
+
return true if table_exists?
|
|
25
26
|
|
|
26
27
|
migration_pending_warning!
|
|
27
28
|
false
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
ActiveSupport.run_load_hooks(:good_job_base_record, self)
|
|
29
32
|
end
|
|
30
33
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob # :nodoc:
|
|
4
|
+
class DiscreteExecution < BaseRecord
|
|
5
|
+
include ErrorEvents
|
|
6
|
+
|
|
7
|
+
self.table_name = 'good_job_executions'
|
|
8
|
+
|
|
9
|
+
belongs_to :execution, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
|
|
10
|
+
belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
|
|
11
|
+
|
|
12
|
+
scope :finished, -> { where.not(finished_at: nil) }
|
|
13
|
+
|
|
14
|
+
alias_attribute :performed_at, :created_at
|
|
15
|
+
|
|
16
|
+
def self.error_event_migrated?
|
|
17
|
+
return true if columns_hash["error_event"].present?
|
|
18
|
+
|
|
19
|
+
migration_pending_warning!
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def number
|
|
24
|
+
serialized_params.fetch('executions', 0) + 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Time between when this job was expected to run and when it started running
|
|
28
|
+
def queue_latency
|
|
29
|
+
created_at - scheduled_at
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Time between when this job started and finished
|
|
33
|
+
def runtime_latency
|
|
34
|
+
(finished_at || Time.current) - performed_at if performed_at
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def last_status_at
|
|
38
|
+
finished_at || created_at
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def status
|
|
42
|
+
if finished_at.present?
|
|
43
|
+
if error.present?
|
|
44
|
+
:retried
|
|
45
|
+
elsif error.present? && job.finished_at.present?
|
|
46
|
+
:discarded
|
|
47
|
+
else
|
|
48
|
+
:succeeded
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
:running
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def display_serialized_params
|
|
56
|
+
serialized_params.merge({
|
|
57
|
+
_good_job_execution: attributes.except('serialized_params'),
|
|
58
|
+
})
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module GoodJob
|
|
3
4
|
# ActiveRecord model that represents an +ActiveJob+ job.
|
|
4
5
|
class Execution < BaseExecution
|
|
5
|
-
include Lockable
|
|
6
|
-
include Filterable
|
|
7
|
-
include Reportable
|
|
8
|
-
|
|
9
6
|
# Raised if something attempts to execute a previously completed Execution again.
|
|
10
7
|
PreviouslyPerformedError = Class.new(StandardError)
|
|
11
8
|
|
|
@@ -70,9 +67,13 @@ module GoodJob
|
|
|
70
67
|
end
|
|
71
68
|
|
|
72
69
|
belongs_to :batch, class_name: 'GoodJob::BatchRecord', optional: true, inverse_of: :executions
|
|
73
|
-
|
|
74
70
|
belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
|
|
75
|
-
|
|
71
|
+
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
|
|
72
|
+
|
|
73
|
+
after_destroy lambda {
|
|
74
|
+
GoodJob::DiscreteExecution.where(active_job_id: active_job_id).delete_all if discrete? # TODO: move into association `dependent: :delete_all` after v4
|
|
75
|
+
self.class.active_job_id(active_job_id).delete_all
|
|
76
|
+
}, if: -> { @_destroy_job }
|
|
76
77
|
|
|
77
78
|
# Get executions with given ActiveJob ID
|
|
78
79
|
# @!method active_job_id
|
|
@@ -201,10 +202,14 @@ module GoodJob
|
|
|
201
202
|
end
|
|
202
203
|
end)
|
|
203
204
|
|
|
204
|
-
# Construct a GoodJob::Execution from an ActiveJob instance.
|
|
205
205
|
def self.build_for_enqueue(active_job, overrides = {})
|
|
206
|
+
new(**enqueue_args(active_job, overrides))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Construct arguments for GoodJob::Execution from an ActiveJob instance.
|
|
210
|
+
def self.enqueue_args(active_job, overrides = {})
|
|
206
211
|
if active_job.priority && GoodJob.configuration.smaller_number_is_higher_priority.nil?
|
|
207
|
-
|
|
212
|
+
GoodJob.deprecator.warn(<<~DEPRECATION)
|
|
208
213
|
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.
|
|
209
214
|
To opt-in to this behavior now, set `config.good_job.smaller_number_is_higher_priority = true` in your GoodJob initializer or application.rb.
|
|
210
215
|
To not opt-in yet, but silence this deprecation warning, set `config.good_job.smaller_number_is_higher_priority = false`.
|
|
@@ -216,8 +221,8 @@ module GoodJob
|
|
|
216
221
|
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
|
217
222
|
priority: active_job.priority || DEFAULT_PRIORITY,
|
|
218
223
|
serialized_params: active_job.serialize,
|
|
219
|
-
scheduled_at: active_job.scheduled_at,
|
|
220
224
|
}
|
|
225
|
+
execution_args[:scheduled_at] = Time.zone.at(active_job.scheduled_at) if active_job.scheduled_at
|
|
221
226
|
execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
|
|
222
227
|
|
|
223
228
|
reenqueued_current_execution = CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
|
|
@@ -238,7 +243,7 @@ module GoodJob
|
|
|
238
243
|
execution_args[:cron_at] = CurrentThread.cron_at
|
|
239
244
|
end
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
execution_args.merge(overrides)
|
|
242
247
|
end
|
|
243
248
|
|
|
244
249
|
# Finds the next eligible Execution, acquire an advisory lock related to it, and
|
|
@@ -254,7 +259,11 @@ module GoodJob
|
|
|
254
259
|
unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(unlock_session: true, select_limit: queue_select_limit) do |executions|
|
|
255
260
|
execution = executions.first
|
|
256
261
|
break if execution.blank?
|
|
257
|
-
|
|
262
|
+
|
|
263
|
+
unless execution.executable?
|
|
264
|
+
result = ExecutionResult.new(value: nil, unexecutable: true)
|
|
265
|
+
break
|
|
266
|
+
end
|
|
258
267
|
|
|
259
268
|
yield(execution) if block_given?
|
|
260
269
|
result = execution.perform
|
|
@@ -298,19 +307,53 @@ 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
|
+
# TODO: these values ideally shouldn't be persisted until the current_execution is finished
|
|
319
|
+
# which will require handling `retry_job` being called from outside the execution context.
|
|
320
|
+
execution.performed_at = nil
|
|
321
|
+
execution.finished_at = nil
|
|
322
|
+
else
|
|
323
|
+
execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
|
|
324
|
+
end
|
|
325
|
+
else
|
|
326
|
+
execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
|
|
327
|
+
execution.make_discrete if discrete_support?
|
|
328
|
+
end
|
|
302
329
|
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
if create_with_advisory_lock
|
|
331
|
+
if execution.persisted?
|
|
332
|
+
execution.advisory_lock
|
|
333
|
+
else
|
|
334
|
+
execution.create_with_advisory_lock = true
|
|
335
|
+
end
|
|
336
|
+
end
|
|
305
337
|
|
|
338
|
+
instrument_payload[:execution] = execution
|
|
306
339
|
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
340
|
|
|
341
|
+
if retried
|
|
342
|
+
CurrentThread.execution_retried = true
|
|
343
|
+
CurrentThread.execution.retried_good_job_id = execution.id unless current_execution.discrete?
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
active_job.provider_job_id = execution.id
|
|
310
347
|
execution
|
|
311
348
|
end
|
|
312
349
|
end
|
|
313
350
|
|
|
351
|
+
def self.format_error(error)
|
|
352
|
+
raise ArgumentError unless error.is_a?(Exception)
|
|
353
|
+
|
|
354
|
+
[error.class.to_s, ERROR_MESSAGE_SEPARATOR, error.message].join
|
|
355
|
+
end
|
|
356
|
+
|
|
314
357
|
# Execute the ActiveJob job this {Execution} represents.
|
|
315
358
|
# @return [ExecutionResult]
|
|
316
359
|
# An array of the return value of the job's +#perform+ method and the
|
|
@@ -320,12 +363,43 @@ module GoodJob
|
|
|
320
363
|
run_callbacks(:perform) do
|
|
321
364
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
|
322
365
|
|
|
366
|
+
discrete_execution = nil
|
|
323
367
|
result = GoodJob::CurrentThread.within do |current_thread|
|
|
324
368
|
current_thread.reset
|
|
325
369
|
current_thread.execution = self
|
|
326
370
|
|
|
327
|
-
|
|
328
|
-
|
|
371
|
+
if performed_at
|
|
372
|
+
current_thread.execution_interrupted = performed_at
|
|
373
|
+
|
|
374
|
+
if discrete?
|
|
375
|
+
interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{performed_at}'"))
|
|
376
|
+
self.error = interrupt_error_string
|
|
377
|
+
self.error_event = ERROR_EVENT_INTERRUPTED if self.class.error_event_migrated?
|
|
378
|
+
|
|
379
|
+
discrete_execution_attrs = {
|
|
380
|
+
error: interrupt_error_string,
|
|
381
|
+
finished_at: Time.current,
|
|
382
|
+
}
|
|
383
|
+
discrete_execution_attrs[:error_event] = GoodJob::ErrorEvents::ERROR_EVENT_ENUMS[GoodJob::ErrorEvents::ERROR_EVENT_INTERRUPTED] if self.class.error_event_migrated?
|
|
384
|
+
discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if discrete?
|
|
389
|
+
transaction do
|
|
390
|
+
now = Time.current
|
|
391
|
+
discrete_execution = discrete_executions.create!(
|
|
392
|
+
job_class: job_class,
|
|
393
|
+
queue_name: queue_name,
|
|
394
|
+
serialized_params: serialized_params,
|
|
395
|
+
scheduled_at: (scheduled_at || created_at),
|
|
396
|
+
created_at: now
|
|
397
|
+
)
|
|
398
|
+
update!(performed_at: now, executions_count: ((executions_count || 0) + 1))
|
|
399
|
+
end
|
|
400
|
+
else
|
|
401
|
+
update!(performed_at: Time.current)
|
|
402
|
+
end
|
|
329
403
|
|
|
330
404
|
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
405
|
value = ActiveJob::Base.execute(active_job_data)
|
|
@@ -336,27 +410,79 @@ module GoodJob
|
|
|
336
410
|
end
|
|
337
411
|
handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
|
|
338
412
|
|
|
413
|
+
error_event = if handled_error == current_thread.error_on_discard
|
|
414
|
+
ERROR_EVENT_DISCARDED
|
|
415
|
+
elsif handled_error == current_thread.error_on_retry
|
|
416
|
+
ERROR_EVENT_RETRIED
|
|
417
|
+
elsif handled_error == current_thread.error_on_retry_stopped
|
|
418
|
+
ERROR_EVENT_RETRY_STOPPED
|
|
419
|
+
elsif handled_error
|
|
420
|
+
ERROR_EVENT_HANDLED
|
|
421
|
+
end
|
|
422
|
+
|
|
339
423
|
instrument_payload.merge!(
|
|
340
424
|
value: value,
|
|
341
425
|
handled_error: handled_error,
|
|
342
|
-
retried: current_thread.
|
|
426
|
+
retried: current_thread.execution_retried,
|
|
427
|
+
error_event: error_event
|
|
343
428
|
)
|
|
344
|
-
ExecutionResult.new(value: value, handled_error: handled_error, retried: current_thread.
|
|
429
|
+
ExecutionResult.new(value: value, handled_error: handled_error, error_event: error_event, retried: current_thread.execution_retried)
|
|
345
430
|
rescue StandardError => e
|
|
431
|
+
error_event = if e.is_a?(GoodJob::InterruptError)
|
|
432
|
+
ERROR_EVENT_INTERRUPTED
|
|
433
|
+
elsif e == current_thread.error_on_retry_stopped
|
|
434
|
+
ERROR_EVENT_RETRY_STOPPED
|
|
435
|
+
else
|
|
436
|
+
ERROR_EVENT_UNHANDLED
|
|
437
|
+
end
|
|
438
|
+
|
|
346
439
|
instrument_payload[:unhandled_error] = e
|
|
347
|
-
ExecutionResult.new(value: nil, unhandled_error: e)
|
|
440
|
+
ExecutionResult.new(value: nil, unhandled_error: e, error_event: error_event)
|
|
348
441
|
end
|
|
349
442
|
end
|
|
350
443
|
|
|
351
444
|
job_error = result.handled_error || result.unhandled_error
|
|
352
|
-
|
|
445
|
+
|
|
446
|
+
if job_error
|
|
447
|
+
error_string = self.class.format_error(job_error)
|
|
448
|
+
self.error = error_string
|
|
449
|
+
self.error_event = result.error_event if self.class.error_event_migrated?
|
|
450
|
+
if discrete_execution
|
|
451
|
+
discrete_execution.error = error_string
|
|
452
|
+
discrete_execution.error_event = result.error_event if discrete_execution.class.error_event_migrated?
|
|
453
|
+
end
|
|
454
|
+
else
|
|
455
|
+
self.error = nil
|
|
456
|
+
self.error_event = nil if self.class.error_event_migrated?
|
|
457
|
+
end
|
|
353
458
|
|
|
354
459
|
reenqueued = result.retried? || retried_good_job_id.present?
|
|
355
460
|
if result.unhandled_error && GoodJob.retry_on_unhandled_error
|
|
356
|
-
|
|
461
|
+
if discrete_execution
|
|
462
|
+
transaction do
|
|
463
|
+
discrete_execution.update!(finished_at: Time.current)
|
|
464
|
+
update!(performed_at: nil, finished_at: nil, retried_good_job_id: nil)
|
|
465
|
+
end
|
|
466
|
+
else
|
|
467
|
+
save!
|
|
468
|
+
end
|
|
357
469
|
elsif GoodJob.preserve_job_records == true || reenqueued || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error) || cron_key.present?
|
|
358
|
-
|
|
359
|
-
|
|
470
|
+
now = Time.current
|
|
471
|
+
if discrete_execution
|
|
472
|
+
if reenqueued
|
|
473
|
+
self.performed_at = nil
|
|
474
|
+
else
|
|
475
|
+
self.finished_at = now
|
|
476
|
+
end
|
|
477
|
+
discrete_execution.finished_at = now
|
|
478
|
+
transaction do
|
|
479
|
+
discrete_execution.save!
|
|
480
|
+
save!
|
|
481
|
+
end
|
|
482
|
+
else
|
|
483
|
+
self.finished_at = now
|
|
484
|
+
save!
|
|
485
|
+
end
|
|
360
486
|
else
|
|
361
487
|
destroy_job
|
|
362
488
|
end
|
|
@@ -371,6 +497,17 @@ module GoodJob
|
|
|
371
497
|
self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
|
|
372
498
|
end
|
|
373
499
|
|
|
500
|
+
def make_discrete
|
|
501
|
+
self.is_discrete = true
|
|
502
|
+
self.id = active_job_id
|
|
503
|
+
self.job_class = serialized_params['job_class']
|
|
504
|
+
self.executions_count ||= 0
|
|
505
|
+
|
|
506
|
+
current_time = Time.current
|
|
507
|
+
self.created_at ||= current_time
|
|
508
|
+
self.scheduled_at ||= current_time
|
|
509
|
+
end
|
|
510
|
+
|
|
374
511
|
# Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
|
|
375
512
|
#
|
|
376
513
|
# @param ignore_deserialization_errors [Boolean]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module GoodJob
|
|
3
4
|
# Stores the results of job execution
|
|
4
5
|
class ExecutionResult
|
|
@@ -8,18 +9,32 @@ module GoodJob
|
|
|
8
9
|
attr_reader :handled_error
|
|
9
10
|
# @return [Exception, nil]
|
|
10
11
|
attr_reader :unhandled_error
|
|
11
|
-
# @return [
|
|
12
|
+
# @return [String, nil]
|
|
13
|
+
attr_reader :error_event
|
|
14
|
+
# @return [Boolean, nil]
|
|
15
|
+
attr_reader :unexecutable
|
|
16
|
+
# @return [Boolean, nil]
|
|
12
17
|
attr_reader :retried
|
|
13
18
|
alias retried? retried
|
|
14
19
|
|
|
15
20
|
# @param value [Object, nil]
|
|
16
21
|
# @param handled_error [Exception, nil]
|
|
17
22
|
# @param unhandled_error [Exception, nil]
|
|
18
|
-
|
|
23
|
+
# @param error_event [String, nil]
|
|
24
|
+
# @param unexecutable [Boolean, nil]
|
|
25
|
+
# @param retried [Boolean, nil]
|
|
26
|
+
def initialize(value:, handled_error: nil, unhandled_error: nil, error_event: nil, unexecutable: nil, retried: false)
|
|
19
27
|
@value = value
|
|
20
28
|
@handled_error = handled_error
|
|
21
29
|
@unhandled_error = unhandled_error
|
|
30
|
+
@error_event = error_event
|
|
31
|
+
@unexecutable = unexecutable
|
|
22
32
|
@retried = retried
|
|
23
33
|
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def succeeded?
|
|
37
|
+
!(handled_error || unhandled_error || unexecutable || retried)
|
|
38
|
+
end
|
|
24
39
|
end
|
|
25
40
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob
|
|
4
|
+
class I18nConfig < ::I18n::Config
|
|
5
|
+
BACKEND = I18n::Backend::Simple.new
|
|
6
|
+
AVAILABLE_LOCALES = GoodJob::Engine.root.join("config/locales").glob("*.yml").map { |path| File.basename(path, ".yml").to_sym }.uniq
|
|
7
|
+
AVAILABLE_LOCALES_SET = AVAILABLE_LOCALES.inject(Set.new) { |set, locale| set << locale.to_s << locale.to_sym }
|
|
8
|
+
|
|
9
|
+
def backend
|
|
10
|
+
BACKEND
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def available_locales
|
|
14
|
+
AVAILABLE_LOCALES
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def available_locales_set
|
|
18
|
+
AVAILABLE_LOCALES_SET
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def default_locale
|
|
22
|
+
GoodJob.configuration.dashboard_default_locale
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module GoodJob
|
|
3
4
|
# ActiveRecord model that represents an +ActiveJob+ job.
|
|
4
5
|
# There is not a table in the database whose discrete rows represents "Jobs".
|
|
@@ -6,10 +7,6 @@ module GoodJob
|
|
|
6
7
|
# A single row from the +good_jobs+ table of executions is fetched to represent a Job.
|
|
7
8
|
#
|
|
8
9
|
class Job < BaseExecution
|
|
9
|
-
include Filterable
|
|
10
|
-
include Lockable
|
|
11
|
-
include Reportable
|
|
12
|
-
|
|
13
10
|
# Raised when an inappropriate action is applied to a Job based on its state.
|
|
14
11
|
ActionForStateMismatchError = Class.new(StandardError)
|
|
15
12
|
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
|
@@ -30,6 +27,11 @@ module GoodJob
|
|
|
30
27
|
|
|
31
28
|
belongs_to :batch, class_name: 'GoodJob::BatchRecord', inverse_of: :jobs, optional: true
|
|
32
29
|
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
|
|
30
|
+
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
|
|
31
|
+
|
|
32
|
+
after_destroy lambda {
|
|
33
|
+
GoodJob::DiscreteExecution.where(active_job_id: active_job_id).delete_all if discrete? # TODO: move into association `dependent: :delete_all` after v4
|
|
34
|
+
}
|
|
33
35
|
|
|
34
36
|
# Only the most-recent unretried execution represents a "Job"
|
|
35
37
|
default_scope { where(retried_good_job_id: nil) }
|
|
@@ -56,6 +58,8 @@ module GoodJob
|
|
|
56
58
|
# Errored but will not be retried
|
|
57
59
|
scope :discarded, -> { finished.where.not(error: nil) }
|
|
58
60
|
|
|
61
|
+
scope :unfinished_undiscrete, -> { where(finished_at: nil, retried_good_job_id: nil, is_discrete: [nil, false]) }
|
|
62
|
+
|
|
59
63
|
# The job's ActiveJob UUID
|
|
60
64
|
# @return [String]
|
|
61
65
|
def id
|
|
@@ -141,6 +145,12 @@ module GoodJob
|
|
|
141
145
|
})
|
|
142
146
|
end
|
|
143
147
|
|
|
148
|
+
# Used when displaying this job in the GoodJob dashboard.
|
|
149
|
+
# @return [String]
|
|
150
|
+
def display_name
|
|
151
|
+
job_class
|
|
152
|
+
end
|
|
153
|
+
|
|
144
154
|
# Tests whether the job is being executed right now.
|
|
145
155
|
# @return [Boolean]
|
|
146
156
|
def running?
|
|
@@ -191,9 +201,11 @@ module GoodJob
|
|
|
191
201
|
|
|
192
202
|
execution.class.transaction(joinable: false, requires_new: true) do
|
|
193
203
|
new_active_job = active_job.retry_job(wait: 0, error: execution.error)
|
|
194
|
-
execution.
|
|
204
|
+
execution.error_event = ERROR_EVENT_RETRIED if execution.error && execution.class.error_event_migrated?
|
|
205
|
+
execution.save!
|
|
195
206
|
end
|
|
196
207
|
end
|
|
208
|
+
|
|
197
209
|
new_active_job
|
|
198
210
|
end
|
|
199
211
|
end
|
|
@@ -212,8 +224,12 @@ module GoodJob
|
|
|
212
224
|
|
|
213
225
|
update_execution = proc do
|
|
214
226
|
execution.update(
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
{
|
|
228
|
+
finished_at: Time.current,
|
|
229
|
+
error: GoodJob::Execution.format_error(job_error),
|
|
230
|
+
}.tap do |attrs|
|
|
231
|
+
attrs[:error_event] = ERROR_EVENT_DISCARDED if self.class.error_event_migrated?
|
|
232
|
+
end
|
|
217
233
|
)
|
|
218
234
|
end
|
|
219
235
|
|