good_job 3.30.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +22 -0
  4. data/app/charts/good_job/scheduled_by_queue_chart.rb +2 -2
  5. data/app/controllers/good_job/cron_entries_controller.rb +0 -8
  6. data/app/controllers/good_job/metrics_controller.rb +1 -1
  7. data/app/controllers/good_job/performance_controller.rb +19 -0
  8. data/app/filters/good_job/base_filter.rb +1 -1
  9. data/app/helpers/good_job/application_helper.rb +1 -0
  10. data/app/models/concerns/good_job/error_events.rb +2 -2
  11. data/app/models/concerns/good_job/filterable.rb +1 -1
  12. data/app/models/good_job/base_execution.rb +544 -50
  13. data/app/models/good_job/cron_entry.rb +0 -2
  14. data/app/models/good_job/discrete_execution.rb +1 -26
  15. data/app/models/good_job/execution.rb +3 -609
  16. data/app/models/good_job/job.rb +37 -116
  17. data/app/models/good_job/process.rb +7 -20
  18. data/app/views/good_job/batches/index.html.erb +11 -15
  19. data/app/views/good_job/jobs/_executions.erb +1 -1
  20. data/app/views/good_job/jobs/_table.erb +1 -1
  21. data/app/views/good_job/jobs/show.html.erb +1 -8
  22. data/app/views/good_job/performance/index.html.erb +43 -0
  23. data/app/views/good_job/shared/_navbar.erb +5 -0
  24. data/config/locales/de.yml +10 -2
  25. data/config/locales/en.yml +10 -2
  26. data/config/locales/es.yml +10 -2
  27. data/config/locales/fr.yml +10 -2
  28. data/config/locales/it.yml +10 -2
  29. data/config/locales/ja.yml +10 -2
  30. data/config/locales/ko.yml +10 -2
  31. data/config/locales/nl.yml +10 -2
  32. data/config/locales/pt-BR.yml +10 -2
  33. data/config/locales/ru.yml +10 -2
  34. data/config/locales/tr.yml +10 -2
  35. data/config/locales/uk.yml +10 -2
  36. data/config/routes.rb +2 -0
  37. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +65 -3
  38. data/lib/good_job/active_job_extensions/batches.rb +1 -1
  39. data/lib/good_job/active_job_extensions/concurrency.rb +10 -10
  40. data/lib/good_job/adapter.rb +13 -24
  41. data/lib/good_job/configuration.rb +5 -1
  42. data/lib/good_job/current_thread.rb +6 -6
  43. data/lib/good_job/job_performer.rb +2 -2
  44. data/lib/good_job/log_subscriber.rb +2 -10
  45. data/lib/good_job/notifier.rb +3 -3
  46. data/lib/good_job/version.rb +1 -1
  47. data/lib/good_job.rb +22 -21
  48. metadata +18 -30
  49. data/lib/generators/good_job/templates/update/migrations/02_create_good_job_settings.rb.erb +0 -20
  50. data/lib/generators/good_job/templates/update/migrations/03_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb.erb +0 -19
  51. data/lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb +0 -35
  52. data/lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb +0 -33
  53. data/lib/generators/good_job/templates/update/migrations/06_create_good_jobs_error_event.rb.erb +0 -16
  54. data/lib/generators/good_job/templates/update/migrations/07_recreate_good_job_cron_indexes_with_conditional.rb.erb +0 -45
  55. data/lib/generators/good_job/templates/update/migrations/08_create_good_job_labels.rb.erb +0 -15
  56. data/lib/generators/good_job/templates/update/migrations/09_create_good_job_labels_index.rb.erb +0 -22
  57. data/lib/generators/good_job/templates/update/migrations/10_remove_good_job_active_id_index.rb.erb +0 -21
  58. data/lib/generators/good_job/templates/update/migrations/11_create_index_good_job_jobs_for_candidate_lookup.rb.erb +0 -19
  59. data/lib/generators/good_job/templates/update/migrations/12_create_good_job_execution_error_backtrace.rb.erb +0 -15
  60. data/lib/generators/good_job/templates/update/migrations/13_create_good_job_process_lock_ids.rb.erb +0 -17
  61. data/lib/generators/good_job/templates/update/migrations/14_create_good_job_process_lock_indexes.rb.erb +0 -37
  62. data/lib/generators/good_job/templates/update/migrations/15_create_good_job_execution_duration.rb.erb +0 -15
@@ -14,27 +14,6 @@ module GoodJob # :nodoc:
14
14
 
15
15
  alias_attribute :performed_at, :created_at
16
16
 
17
- def self.error_event_migrated?
18
- return true if columns_hash["error_event"].present?
19
-
20
- migration_pending_warning!
21
- false
22
- end
23
-
24
- def self.backtrace_migrated?
25
- return true if columns_hash["error_backtrace"].present?
26
-
27
- migration_pending_warning!
28
- false
29
- end
30
-
31
- def self.monotonic_duration_migrated?
32
- return true if columns_hash["duration"].present?
33
-
34
- migration_pending_warning!
35
- false
36
- end
37
-
38
17
  def number
39
18
  serialized_params.fetch('executions', 0) + 1
40
19
  end
@@ -46,11 +25,7 @@ module GoodJob # :nodoc:
46
25
 
47
26
  # Monotonic time between when this job started and finished
48
27
  def runtime_latency
49
- if self.class.monotonic_duration_migrated?
50
- duration
51
- elsif performed_at
52
- (finished_at || Time.current) - performed_at
53
- end
28
+ duration
54
29
  end
55
30
 
56
31
  def last_status_at
@@ -1,614 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GoodJob
4
- # Active Record model that represents an +ActiveJob+ job.
5
- class Execution < BaseExecution
6
- # Raised if something attempts to execute a previously completed Execution again.
7
- PreviouslyPerformedError = Class.new(StandardError)
8
-
9
- # String separating Error Class from Error Message
10
- ERROR_MESSAGE_SEPARATOR = ": "
11
-
12
- # ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
13
- DEFAULT_QUEUE_NAME = 'default'
14
- # ActiveJob jobs without a +priority+ attribute are given this priority.
15
- DEFAULT_PRIORITY = 0
16
-
17
- self.table_name = 'good_jobs'
18
- self.advisory_lockable_column = 'active_job_id'
19
- self.implicit_order_column = 'created_at'
20
-
21
- define_model_callbacks :perform
22
- define_model_callbacks :perform_unlocked, only: :after
23
-
24
- set_callback :perform, :around, :reset_batch_values
25
- set_callback :perform_unlocked, :after, :continue_discard_or_finish_batch
26
-
27
- # Parse a string representing a group of queues into a more readable data
28
- # structure.
29
- # @param string [String] Queue string
30
- # @return [Hash]
31
- # How to match a given queue. It can have the following keys and values:
32
- # - +{ all: true }+ indicates that all queues match.
33
- # - +{ exclude: Array<String> }+ indicates the listed queue names should
34
- # not match.
35
- # - +{ include: Array<String> }+ indicates the listed queue names should
36
- # match.
37
- # - +{ include: Array<String>, ordered_queues: true }+ indicates the listed
38
- # queue names should match, and dequeue should respect queue order.
39
- # @example
40
- # GoodJob::Execution.queue_parser('-queue1,queue2')
41
- # => { exclude: [ 'queue1', 'queue2' ] }
42
- def self.queue_parser(string)
43
- string = string.strip.presence || '*'
44
-
45
- case string.first
46
- when '-'
47
- exclude_queues = true
48
- string = string[1..]
49
- when '+'
50
- ordered_queues = true
51
- string = string[1..]
52
- end
53
-
54
- queues = string.split(',').map(&:strip)
55
-
56
- if queues.include?('*')
57
- { all: true }
58
- elsif exclude_queues
59
- { exclude: queues }
60
- elsif ordered_queues
61
- {
62
- include: queues,
63
- ordered_queues: true,
64
- }
65
- else
66
- { include: queues }
67
- end
68
- end
69
-
70
- belongs_to :batch, class_name: 'GoodJob::BatchRecord', optional: true, inverse_of: :executions
71
- belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
72
- 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
73
-
74
- after_destroy lambda {
75
- GoodJob::DiscreteExecution.where(active_job_id: active_job_id).delete_all if discrete? # TODO: move into association `dependent: :delete_all` after v4
76
- self.class.active_job_id(active_job_id).delete_all
77
- }, if: -> { @_destroy_job }
78
-
79
- # Get executions with given ActiveJob ID
80
- # @!method active_job_id(active_job_id)
81
- # @!scope class
82
- # @param active_job_id [String]
83
- # ActiveJob ID
84
- # @return [ActiveRecord::Relation]
85
- scope :active_job_id, ->(active_job_id) { where(active_job_id: active_job_id) }
86
-
87
- # Get executions that have not yet finished (succeeded or discarded).
88
- # @!method unfinished
89
- # @!scope class
90
- # @return [ActiveRecord::Relation]
91
- scope :unfinished, -> { where(finished_at: nil) }
92
-
93
- # Get executions that are not scheduled for a later time than now (i.e. jobs that
94
- # are not scheduled or scheduled for earlier than the current time).
95
- # @!method only_scheduled
96
- # @!scope class
97
- # @return [ActiveRecord::Relation]
98
- scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(bind_value('scheduled_at', DateTime.current, ActiveRecord::Type::DateTime))).or(where(scheduled_at: nil)) }
99
-
100
- # Order executions by priority (highest priority first).
101
- # @!method priority_ordered
102
- # @!scope class
103
- # @return [ActiveRecord::Relation]
104
- scope :priority_ordered, (lambda do
105
- if GoodJob.configuration.smaller_number_is_higher_priority
106
- order('priority ASC NULLS LAST')
107
- else
108
- order('priority DESC NULLS LAST')
109
- end
110
- end)
111
-
112
- # Order executions by created_at, for first-in first-out
113
- # @!method creation_ordered
114
- # @!scope class
115
- # @return [ActiveRecord:Relation]
116
- scope :creation_ordered, -> { order(created_at: :asc) }
117
-
118
- # Order executions for de-queueing
119
- # @!method dequeueing_ordered(parsed_queues)
120
- # @!scope class
121
- # @param parsed_queues [Hash]
122
- # optional output of .queue_parser, parsed queues, will be used for
123
- # ordered queues.
124
- # @return [ActiveRecord::Relation]
125
- scope :dequeueing_ordered, (lambda do |parsed_queues|
126
- relation = self
127
- relation = relation.queue_ordered(parsed_queues[:include]) if parsed_queues && parsed_queues[:ordered_queues] && parsed_queues[:include]
128
- relation = relation.priority_ordered.creation_ordered
129
-
130
- relation
131
- end)
132
-
133
- # Order executions in order of queues in array param
134
- # @!method queue_ordered(queues)
135
- # @!scope class
136
- # @param queues [Array<string] ordered names of queues
137
- # @return [ActiveRecord::Relation]
138
- scope :queue_ordered, (lambda do |queues|
139
- clauses = queues.map.with_index do |queue_name, index|
140
- "WHEN queue_name = '#{queue_name}' THEN #{index}"
141
- end
142
-
143
- order(
144
- Arel.sql("(CASE #{clauses.join(' ')} ELSE #{queues.length} END)")
145
- )
146
- end)
147
-
148
- # Order jobs by scheduled or created (oldest first).
149
- # @!method schedule_ordered
150
- # @!scope class
151
- # @return [ActiveRecord::Relation]
152
- scope :schedule_ordered, -> { order(coalesce_scheduled_at_created_at.asc) }
153
-
154
- # Get completed jobs before the given timestamp. If no timestamp is
155
- # provided, get *all* completed jobs. By default, GoodJob
156
- # destroys jobs after they're completed, meaning this returns no jobs.
157
- # However, if you have changed {GoodJob.preserve_job_records}, this may
158
- # find completed Jobs.
159
- # @!method finished(timestamp = nil)
160
- # @!scope class
161
- # @param timestamp (Float)
162
- # Get jobs that finished before this time (in epoch time).
163
- # @return [ActiveRecord::Relation]
164
- scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(bind_value('finished_at', timestamp, ActiveRecord::Type::DateTime))) : where.not(finished_at: nil) }
165
-
166
- # Get Jobs that started but not finished yet.
167
- # @!method running
168
- # @!scope class
169
- # @return [ActiveRecord::Relation]
170
- scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
171
-
172
- # Get Jobs that do not have subsequent retries
173
- # @!method running
174
- # @!scope class
175
- # @return [ActiveRecord::Relation]
176
- scope :head, -> { where(retried_good_job_id: nil) }
177
-
178
- # Get Jobs have errored that will not be retried further
179
- # @!method running
180
- # @!scope class
181
- # @return [ActiveRecord::Relation]
182
- scope :dead, -> { head.where.not(error: nil) }
183
-
184
- # Get Jobs on queues that match the given queue string.
185
- # @!method queue_string(string)
186
- # @!scope class
187
- # @param string [String]
188
- # A string expression describing what queues to select. See
189
- # {Execution.queue_parser} or
190
- # {file:README.md#optimize-queues-threads-and-processes} for more details
191
- # on the format of the string. Note this only handles individual
192
- # semicolon-separated segments of that string format.
193
- # @return [ActiveRecord::Relation]
194
- scope :queue_string, (lambda do |string|
195
- parsed = queue_parser(string)
196
-
197
- if parsed[:all]
198
- all
199
- elsif parsed[:exclude]
200
- where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
201
- elsif parsed[:include]
202
- where(queue_name: parsed[:include])
203
- end
204
- end)
205
-
206
- def self.build_for_enqueue(active_job, overrides = {})
207
- new(**enqueue_args(active_job, overrides))
208
- end
209
-
210
- # Construct arguments for GoodJob::Execution from an ActiveJob instance.
211
- def self.enqueue_args(active_job, overrides = {})
212
- if active_job.priority && GoodJob.configuration.smaller_number_is_higher_priority.nil?
213
- GoodJob.deprecator.warn(<<~DEPRECATION)
214
- 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.
215
- To opt-in to this behavior now, set `config.good_job.smaller_number_is_higher_priority = true` in your GoodJob initializer or application.rb.
216
- To not opt-in yet, but silence this deprecation warning, set `config.good_job.smaller_number_is_higher_priority = false`.
217
- DEPRECATION
218
- end
219
-
220
- execution_args = {
221
- active_job_id: active_job.job_id,
222
- queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
223
- priority: active_job.priority || DEFAULT_PRIORITY,
224
- serialized_params: active_job.serialize,
225
- }
226
- execution_args[:scheduled_at] = Time.zone.at(active_job.scheduled_at) if active_job.scheduled_at
227
- execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
228
-
229
- if active_job.respond_to?(:good_job_labels) && active_job.good_job_labels.any? && labels_migrated?
230
- labels = active_job.good_job_labels.dup
231
- labels.map! { |label| label.to_s.strip.presence }
232
- labels.tap(&:compact!).tap(&:uniq!)
233
- execution_args[:labels] = labels
234
- end
235
-
236
- reenqueued_current_execution = CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
237
- current_execution = CurrentThread.execution
238
-
239
- if reenqueued_current_execution
240
- if GoodJob::BatchRecord.migrated?
241
- execution_args[:batch_id] = current_execution.batch_id
242
- execution_args[:batch_callback_id] = current_execution.batch_callback_id
243
- end
244
- execution_args[:cron_key] = current_execution.cron_key
245
- else
246
- if GoodJob::BatchRecord.migrated?
247
- execution_args[:batch_id] = GoodJob::Batch.current_batch_id
248
- execution_args[:batch_callback_id] = GoodJob::Batch.current_batch_callback_id
249
- end
250
- execution_args[:cron_key] = CurrentThread.cron_key
251
- execution_args[:cron_at] = CurrentThread.cron_at
252
- end
253
-
254
- execution_args.merge(overrides)
255
- end
256
-
257
- # Finds the next eligible Execution, acquire an advisory lock related to it, and
258
- # executes the job.
259
- # @yield [Execution, nil] The next eligible Execution, or +nil+ if none found, before it is performed.
260
- # @return [ExecutionResult, nil]
261
- # If a job was executed, returns an array with the {Execution} record, the
262
- # return value for the job's +#perform+ method, and the exception the job
263
- # raised, if any (if the job raised, then the second array entry will be
264
- # +nil+). If there were no jobs to execute, returns +nil+.
265
- def self.perform_with_advisory_lock(lock_id:, parsed_queues: nil, queue_select_limit: nil)
266
- execution = nil
267
- result = nil
268
-
269
- unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(select_limit: queue_select_limit) do |executions|
270
- execution = executions.first
271
- if execution&.executable?
272
- yield(execution) if block_given?
273
- result = execution.perform(lock_id: lock_id)
274
- else
275
- execution = nil
276
- yield(nil) if block_given?
277
- end
278
- end
279
-
280
- execution&.run_callbacks(:perform_unlocked)
281
- result
282
- end
283
-
284
- # Fetches the scheduled execution time of the next eligible Execution(s).
285
- # @param after [DateTime]
286
- # @param limit [Integer]
287
- # @param now_limit [Integer, nil]
288
- # @return [Array<DateTime>]
289
- def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
290
- query = advisory_unlocked.unfinished.schedule_ordered
291
-
292
- after ||= Time.current
293
- after_bind = bind_value('scheduled_at', after, ActiveRecord::Type::DateTime)
294
- after_query = query.where(arel_table['scheduled_at'].gt(after_bind)).or query.where(scheduled_at: nil).where(arel_table['created_at'].gt(after_bind))
295
- after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
296
-
297
- if now_limit&.positive?
298
- now_query = query.where(arel_table['scheduled_at'].lt(bind_value('scheduled_at', Time.current, ActiveRecord::Type::DateTime))).or query.where(scheduled_at: nil)
299
- now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
300
- end
301
-
302
- Array(now_at) + after_at
303
- end
304
-
305
- # Places an ActiveJob job on a queue by creating a new {Execution} record.
306
- # @param active_job [ActiveJob::Base]
307
- # The job to enqueue.
308
- # @param scheduled_at [Float]
309
- # Epoch timestamp when the job should be executed, if blank will delegate to the ActiveJob instance
310
- # @param create_with_advisory_lock [Boolean]
311
- # Whether to establish a lock on the {Execution} record after it is created.
312
- # @return [Execution]
313
- # The new {Execution} instance representing the queued ActiveJob job.
314
- def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
315
- 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|
316
- current_execution = CurrentThread.execution
317
-
318
- retried = current_execution && current_execution.active_job_id == active_job.job_id
319
- if retried
320
- if current_execution.discrete?
321
- execution = current_execution
322
- execution.assign_attributes(enqueue_args(active_job, { scheduled_at: scheduled_at }))
323
- execution.scheduled_at ||= Time.current
324
- # TODO: these values ideally shouldn't be persisted until the current_execution is finished
325
- # which will require handling `retry_job` being called from outside the execution context.
326
- execution.performed_at = nil
327
- execution.finished_at = nil
328
- else
329
- execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
330
- end
331
- else
332
- execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
333
- execution.make_discrete if discrete_support?
334
- end
335
-
336
- if create_with_advisory_lock
337
- if execution.persisted?
338
- execution.advisory_lock
339
- else
340
- execution.create_with_advisory_lock = true
341
- end
342
- end
343
-
344
- instrument_payload[:execution] = execution
345
- execution.save!
346
-
347
- if retried
348
- CurrentThread.execution_retried = execution
349
- CurrentThread.execution.retried_good_job_id = execution.id unless current_execution.discrete?
350
- else
351
- CurrentThread.execution_retried = nil
352
- end
353
-
354
- active_job.provider_job_id = execution.id
355
- execution
356
- end
357
- end
358
-
359
- def self.format_error(error)
360
- raise ArgumentError unless error.is_a?(Exception)
361
-
362
- [error.class.to_s, ERROR_MESSAGE_SEPARATOR, error.message].join
363
- end
364
-
365
- # Execute the ActiveJob job this {Execution} represents.
366
- # @return [ExecutionResult]
367
- # An array of the return value of the job's +#perform+ method and the
368
- # exception raised by the job, if any. If the job completed successfully,
369
- # the second array entry (the exception) will be +nil+ and vice versa.
370
- def perform(lock_id:)
371
- run_callbacks(:perform) do
372
- raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
373
-
374
- job_performed_at = Time.current
375
- monotonic_start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
376
- discrete_execution = nil
377
- result = GoodJob::CurrentThread.within do |current_thread|
378
- current_thread.reset
379
- current_thread.execution = self
380
-
381
- existing_performed_at = performed_at
382
- if existing_performed_at
383
- current_thread.execution_interrupted = existing_performed_at
384
-
385
- if discrete?
386
- interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{existing_performed_at}'"))
387
- self.error = interrupt_error_string
388
- self.error_event = ERROR_EVENT_INTERRUPTED if self.class.error_event_migrated?
389
- monotonic_duration = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - monotonic_start).seconds
390
-
391
- discrete_execution_attrs = {
392
- error: interrupt_error_string,
393
- finished_at: job_performed_at,
394
- }
395
- discrete_execution_attrs[:error_event] = GoodJob::ErrorEvents::ERROR_EVENT_ENUMS[GoodJob::ErrorEvents::ERROR_EVENT_INTERRUPTED] if self.class.error_event_migrated?
396
- discrete_execution_attrs[:duration] = monotonic_duration if GoodJob::DiscreteExecution.monotonic_duration_migrated?
397
- discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
398
- end
399
- end
400
-
401
- if discrete?
402
- transaction do
403
- discrete_execution_attrs = {
404
- job_class: job_class,
405
- queue_name: queue_name,
406
- serialized_params: serialized_params,
407
- scheduled_at: (scheduled_at || created_at),
408
- created_at: job_performed_at,
409
- }
410
- discrete_execution_attrs[:process_id] = lock_id if GoodJob::DiscreteExecution.columns_hash.key?("process_id")
411
-
412
- execution_attrs = {
413
- performed_at: job_performed_at,
414
- executions_count: ((executions_count || 0) + 1),
415
- }
416
- if GoodJob::Execution.columns_hash.key?("locked_by_id")
417
- execution_attrs[:locked_by_id] = lock_id
418
- execution_attrs[:locked_at] = Time.current
419
- end
420
-
421
- discrete_execution = discrete_executions.create!(discrete_execution_attrs)
422
- update!(execution_attrs)
423
- end
424
- else
425
- execution_attrs = {
426
- performed_at: job_performed_at,
427
- }
428
- if GoodJob::Execution.columns_hash.key?("locked_by_id")
429
- execution_attrs[:locked_by_id] = lock_id
430
- execution_attrs[:locked_at] = Time.current
431
- end
432
-
433
- update!(execution_attrs)
434
- end
435
-
436
- ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do |instrument_payload|
437
- value = ActiveJob::Base.execute(active_job_data)
438
-
439
- if value.is_a?(Exception)
440
- handled_error = value
441
- value = nil
442
- end
443
- handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
444
-
445
- error_event = if handled_error == current_thread.error_on_discard
446
- ERROR_EVENT_DISCARDED
447
- elsif handled_error == current_thread.error_on_retry
448
- ERROR_EVENT_RETRIED
449
- elsif handled_error == current_thread.error_on_retry_stopped
450
- ERROR_EVENT_RETRY_STOPPED
451
- elsif handled_error
452
- ERROR_EVENT_HANDLED
453
- end
454
-
455
- instrument_payload.merge!(
456
- value: value,
457
- handled_error: handled_error,
458
- retried: current_thread.execution_retried.present?,
459
- error_event: error_event
460
- )
461
- ExecutionResult.new(value: value, handled_error: handled_error, error_event: error_event, retried: current_thread.execution_retried)
462
- rescue StandardError => e
463
- error_event = if e.is_a?(GoodJob::InterruptError)
464
- ERROR_EVENT_INTERRUPTED
465
- elsif e == current_thread.error_on_retry_stopped
466
- ERROR_EVENT_RETRY_STOPPED
467
- else
468
- ERROR_EVENT_UNHANDLED
469
- end
470
-
471
- instrument_payload[:unhandled_error] = e
472
- ExecutionResult.new(value: nil, unhandled_error: e, error_event: error_event)
473
- end
474
- end
475
-
476
- job_attributes = if self.class.columns_hash.key?("locked_by_id")
477
- { locked_by_id: nil, locked_at: nil }
478
- else
479
- {}
480
- end
481
-
482
- job_error = result.handled_error || result.unhandled_error
483
- if job_error
484
- error_string = self.class.format_error(job_error)
485
-
486
- job_attributes[:error] = error_string
487
- job_attributes[:error_event] = result.error_event if self.class.error_event_migrated?
488
- if discrete_execution
489
- discrete_execution.error = error_string
490
- discrete_execution.error_event = result.error_event
491
- discrete_execution.error_backtrace = job_error.backtrace if discrete_execution.class.backtrace_migrated?
492
- end
493
- else
494
- job_attributes[:error] = nil
495
- job_attributes[:error_event] = nil
496
- end
497
- job_attributes.delete(:error_event) unless self.class.error_event_migrated?
498
-
499
- job_finished_at = Time.current
500
- monotonic_duration = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - monotonic_start).seconds
501
- job_attributes[:finished_at] = job_finished_at
502
- if discrete_execution
503
- discrete_execution.finished_at = job_finished_at
504
- discrete_execution.duration = monotonic_duration if GoodJob::DiscreteExecution.monotonic_duration_migrated?
505
- end
506
-
507
- retry_unhandled_error = result.unhandled_error && GoodJob.retry_on_unhandled_error
508
- reenqueued = result.retried? || retried_good_job_id.present? || retry_unhandled_error
509
- if reenqueued
510
- if discrete_execution
511
- job_attributes[:performed_at] = nil
512
- job_attributes[:finished_at] = nil
513
- else
514
- job_attributes[:retried_good_job_id] = retried_good_job_id
515
- job_attributes[:finished_at] = nil if retry_unhandled_error
516
- end
517
- end
518
-
519
- preserve_unhandled = (result.unhandled_error && (GoodJob.retry_on_unhandled_error || GoodJob.preserve_job_records == :on_unhandled_error))
520
- if GoodJob.preserve_job_records == true || reenqueued || preserve_unhandled || cron_key.present?
521
- if discrete_execution
522
- transaction do
523
- discrete_execution.save!
524
- update!(job_attributes)
525
- end
526
- else
527
- update!(job_attributes)
528
- end
529
- else
530
- destroy_job
531
- end
532
-
533
- result
534
- end
535
- end
536
-
537
- # Tests whether this job is safe to be executed by this thread.
538
- # @return [Boolean]
539
- def executable?
540
- reload.finished_at.blank?
541
- rescue ActiveRecord::RecordNotFound
542
- false
543
- end
544
-
545
- def make_discrete
546
- self.is_discrete = true
547
- self.id = active_job_id
548
- self.job_class = serialized_params['job_class']
549
- self.executions_count ||= 0
550
-
551
- current_time = Time.current
552
- self.created_at ||= current_time
553
- self.scheduled_at ||= current_time
554
- end
555
-
556
- # Return formatted serialized_params for display in the dashboard
557
- # @return [Hash]
558
- def display_serialized_params
559
- serialized_params.merge({
560
- _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
561
- })
562
- end
563
-
564
- def running?
565
- if has_attribute?(:locktype)
566
- self['locktype'].present?
567
- else
568
- advisory_locked?
569
- end
570
- end
571
-
572
- def number
573
- serialized_params.fetch('executions', 0) + 1
574
- end
575
-
576
- # Time between when this job was expected to run and when it started running
577
- def queue_latency
578
- now = Time.zone.now
579
- expected_start = scheduled_at || created_at
580
- actual_start = performed_at || finished_at || now
581
-
582
- actual_start - expected_start unless expected_start >= now
583
- end
584
-
585
- # Time between when this job started and finished
586
- def runtime_latency
587
- (finished_at || Time.zone.now) - performed_at if performed_at
588
- end
589
-
590
- # Destroys this execution and all executions within the same job
591
- def destroy_job
592
- @_destroy_job = true
593
- destroy!
594
- ensure
595
- @_destroy_job = false
596
- end
597
-
598
- def job_state
599
- state = { queue_name: queue_name }
600
- state[:scheduled_at] = scheduled_at if scheduled_at
601
- state
602
- end
603
-
604
- private
605
-
606
- def reset_batch_values(&block)
607
- GoodJob::Batch.within_thread(batch_id: nil, batch_callback_id: nil, &block)
608
- end
609
-
610
- def continue_discard_or_finish_batch
611
- batch._continue_discard_or_finish(self) if GoodJob::BatchRecord.migrated? && batch.present?
612
- end
4
+ # Created at the time a Job begins executing.
5
+ # Behavior from +DiscreteExecution+ will be merged into this class.
6
+ class Execution < DiscreteExecution
613
7
  end
614
8
  end