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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +270 -0
  3. data/README.md +33 -13
  4. data/app/controllers/good_job/application_controller.rb +19 -10
  5. data/app/controllers/good_job/batches_controller.rb +1 -0
  6. data/app/controllers/good_job/cron_entries_controller.rb +2 -1
  7. data/app/controllers/good_job/frontends_controller.rb +1 -0
  8. data/app/controllers/good_job/jobs_controller.rb +1 -0
  9. data/app/controllers/good_job/processes_controller.rb +1 -0
  10. data/app/filters/good_job/base_filter.rb +1 -0
  11. data/app/filters/good_job/batches_filter.rb +1 -0
  12. data/app/filters/good_job/jobs_filter.rb +1 -0
  13. data/app/helpers/good_job/application_helper.rb +1 -1
  14. data/app/models/concerns/good_job/error_events.rb +45 -0
  15. data/app/models/concerns/good_job/filterable.rb +1 -0
  16. data/app/models/{good_job → concerns/good_job}/lockable.rb +3 -0
  17. data/app/models/concerns/good_job/reportable.rb +1 -0
  18. data/app/models/good_job/active_record_parent_class.rb +9 -0
  19. data/app/models/good_job/base_execution.rb +21 -1
  20. data/app/models/good_job/base_record.rb +6 -3
  21. data/app/models/good_job/cron_entry.rb +1 -0
  22. data/app/models/good_job/discrete_execution.rb +61 -0
  23. data/app/models/good_job/execution.rb +162 -25
  24. data/app/models/good_job/execution_result.rb +17 -2
  25. data/app/models/good_job/i18n_config.rb +25 -0
  26. data/app/models/good_job/job.rb +23 -7
  27. data/app/models/good_job/process.rb +66 -14
  28. data/app/views/good_job/batches/_jobs.erb +1 -1
  29. data/app/views/good_job/jobs/_executions.erb +1 -1
  30. data/app/views/good_job/jobs/_table.erb +18 -5
  31. data/app/views/good_job/jobs/show.html.erb +31 -5
  32. data/app/views/good_job/processes/index.html.erb +6 -2
  33. data/app/views/good_job/shared/_navbar.erb +1 -1
  34. data/config/locales/de.yml +9 -1
  35. data/config/locales/en.yml +9 -1
  36. data/config/locales/es.yml +9 -1
  37. data/config/locales/fr.yml +71 -63
  38. data/config/locales/ja.yml +227 -0
  39. data/config/locales/nl.yml +11 -3
  40. data/config/locales/ru.yml +9 -1
  41. data/config/locales/tr.yml +227 -0
  42. data/config/locales/{ua.yml → uk.yml} +34 -2
  43. data/config/routes.rb +1 -0
  44. data/exe/good_job +1 -0
  45. data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
  46. data/lib/generators/good_job/install_generator.rb +1 -0
  47. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +23 -1
  48. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +3 -1
  49. data/lib/generators/good_job/templates/update/migrations/02_create_good_job_settings.rb.erb +1 -0
  50. data/lib/generators/good_job/templates/update/migrations/03_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb.erb +1 -0
  51. data/lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb +1 -0
  52. data/lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb +33 -0
  53. data/lib/generators/good_job/templates/update/migrations/06_create_good_jobs_error_event.rb.erb +16 -0
  54. data/lib/generators/good_job/update_generator.rb +1 -0
  55. data/lib/good_job/active_job_extensions/batches.rb +1 -0
  56. data/lib/good_job/active_job_extensions/concurrency.rb +2 -1
  57. data/lib/good_job/active_job_extensions/interrupt_errors.rb +1 -0
  58. data/lib/good_job/active_job_extensions/notify_options.rb +1 -0
  59. data/lib/good_job/adapter.rb +22 -8
  60. data/lib/good_job/assignable_connection.rb +1 -0
  61. data/lib/good_job/bulk.rb +1 -0
  62. data/lib/good_job/capsule.rb +22 -14
  63. data/lib/good_job/cleanup_tracker.rb +2 -1
  64. data/lib/good_job/cli.rb +2 -1
  65. data/lib/good_job/configuration.rb +13 -6
  66. data/lib/good_job/cron_manager.rb +3 -3
  67. data/lib/good_job/current_thread.rb +15 -0
  68. data/lib/good_job/daemon.rb +1 -0
  69. data/lib/good_job/dependencies.rb +5 -26
  70. data/lib/good_job/engine.rb +20 -12
  71. data/lib/good_job/interrupt_error.rb +1 -0
  72. data/lib/good_job/job_performer.rb +1 -0
  73. data/lib/good_job/log_subscriber.rb +1 -0
  74. data/lib/good_job/metrics.rb +57 -0
  75. data/lib/good_job/multi_scheduler.rb +1 -0
  76. data/lib/good_job/notifier/{process_registration.rb → process_heartbeat.rb} +12 -1
  77. data/lib/good_job/notifier.rb +18 -16
  78. data/lib/good_job/poller.rb +3 -3
  79. data/lib/good_job/scheduler.rb +30 -11
  80. data/lib/good_job/version.rb +5 -1
  81. data/lib/good_job.rb +67 -12
  82. metadata +15 -6
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ ActiveRecordParentClass = if GoodJob.active_record_parent_class
5
+ Object.const_get(GoodJob.active_record_parent_class)
6
+ else
7
+ ActiveRecord::Base
8
+ end
9
+ end
@@ -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 < Object.const_get(GoodJob.active_record_parent_class)
8
+ class BaseRecord < ActiveRecordParentClass
8
9
  self.abstract_class = true
9
10
 
10
11
  def self.migration_pending_warning!
11
- ActiveSupport::Deprecation.warn(<<~DEPRECATION)
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 connection.table_exists?(table_name)
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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "concurrent/hash"
3
4
  require "concurrent/scheduled_task"
4
5
  require "fugit"
@@ -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
- after_destroy -> { self.class.active_job_id(active_job_id).delete_all }, if: -> { @_destroy_job }
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
- ActiveSupport::Deprecation.warn(<<~DEPRECATION)
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
- new(**execution_args.merge(overrides))
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
- break :unlocked unless execution&.executable?
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
- 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
+ # 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
- execution.create_with_advisory_lock = create_with_advisory_lock
304
- instrument_payload[:execution] = execution
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
- current_thread.execution_interrupted = performed_at if performed_at
328
- update!(performed_at: Time.current)
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.error_on_retry.present?
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.error_on_retry.present?)
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
- self.error = [job_error.class, ERROR_MESSAGE_SEPARATOR, job_error.message].join if job_error
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
- save!
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
- self.finished_at = Time.current
359
- save!
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 [Exception, nil]
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
- def initialize(value:, handled_error: nil, unhandled_error: nil, retried: false)
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
@@ -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.save
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
- finished_at: Time.current,
216
- error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
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