good_job 4.1.0 → 4.2.0

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +10 -10
  4. data/app/charts/good_job/performance_index_chart.rb +1 -1
  5. data/app/charts/good_job/performance_show_chart.rb +1 -1
  6. data/app/controllers/good_job/application_controller.rb +1 -1
  7. data/app/controllers/good_job/batches_controller.rb +6 -0
  8. data/app/controllers/good_job/frontends_controller.rb +6 -2
  9. data/app/controllers/good_job/performance_controller.rb +1 -1
  10. data/app/frontend/good_job/icons.svg +79 -0
  11. data/app/frontend/good_job/style.css +5 -0
  12. data/app/helpers/good_job/icons_helper.rb +8 -5
  13. data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
  14. data/app/models/concerns/good_job/error_events.rb +2 -2
  15. data/app/models/concerns/good_job/reportable.rb +8 -12
  16. data/app/models/good_job/batch.rb +31 -9
  17. data/app/models/good_job/batch_record.rb +19 -20
  18. data/app/models/good_job/discrete_execution.rb +6 -59
  19. data/app/models/good_job/execution.rb +59 -4
  20. data/app/models/good_job/execution_result.rb +6 -6
  21. data/app/models/good_job/job.rb +543 -12
  22. data/app/models/good_job/process.rb +14 -3
  23. data/app/views/good_job/batches/_jobs.erb +1 -1
  24. data/app/views/good_job/batches/_table.erb +7 -1
  25. data/app/views/good_job/batches/show.html.erb +8 -0
  26. data/app/views/good_job/jobs/index.html.erb +1 -1
  27. data/app/views/layouts/good_job/application.html.erb +7 -7
  28. data/config/brakeman.ignore +75 -0
  29. data/config/locales/de.yml +54 -49
  30. data/config/locales/en.yml +5 -0
  31. data/config/locales/es.yml +19 -14
  32. data/config/locales/fr.yml +5 -0
  33. data/config/locales/it.yml +5 -0
  34. data/config/locales/ja.yml +10 -5
  35. data/config/locales/ko.yml +9 -4
  36. data/config/locales/nl.yml +5 -0
  37. data/config/locales/pt-BR.yml +5 -0
  38. data/config/locales/ru.yml +5 -0
  39. data/config/locales/tr.yml +5 -0
  40. data/config/locales/uk.yml +6 -1
  41. data/config/routes.rb +8 -4
  42. data/lib/good_job/active_job_extensions/concurrency.rb +109 -98
  43. data/lib/good_job/adapter/inline_buffer.rb +73 -0
  44. data/lib/good_job/adapter.rb +59 -53
  45. data/lib/good_job/capsule_tracker.rb +2 -2
  46. data/lib/good_job/configuration.rb +13 -12
  47. data/lib/good_job/cron_manager.rb +1 -3
  48. data/lib/good_job/current_thread.rb +4 -4
  49. data/lib/good_job/notifier/process_heartbeat.rb +3 -2
  50. data/lib/good_job/version.rb +1 -1
  51. data/lib/good_job.rb +6 -5
  52. metadata +6 -20
  53. data/app/models/good_job/base_execution.rb +0 -605
  54. data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
  55. data/app/views/good_job/shared/icons/_check.html.erb +0 -5
  56. data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
  57. data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
  58. data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
  59. data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
  60. data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
  61. data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
  62. data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
  63. data/app/views/good_job/shared/icons/_info.html.erb +0 -4
  64. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
  65. data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
  66. data/app/views/good_job/shared/icons/_play.html.erb +0 -4
  67. data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
  68. data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
  69. data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
@@ -1,605 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GoodJob
4
- # Active Record model to share behavior between {Job} and {Execution} models
5
- # which both read out of the same table.
6
- class BaseExecution < BaseRecord
7
- self.abstract_class = true
8
-
9
- include AdvisoryLockable
10
- include ErrorEvents
11
- include Filterable
12
- include Reportable
13
-
14
- # Raised if something attempts to execute a previously completed Execution again.
15
- PreviouslyPerformedError = Class.new(StandardError)
16
-
17
- # String separating Error Class from Error Message
18
- ERROR_MESSAGE_SEPARATOR = ": "
19
-
20
- # ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
21
- DEFAULT_QUEUE_NAME = 'default'
22
- # ActiveJob jobs without a +priority+ attribute are given this priority.
23
- DEFAULT_PRIORITY = 0
24
-
25
- self.advisory_lockable_column = 'active_job_id'
26
- self.implicit_order_column = 'created_at'
27
-
28
- self.ignored_columns += ["is_discrete"]
29
-
30
- define_model_callbacks :perform
31
- define_model_callbacks :perform_unlocked, only: :after
32
-
33
- set_callback :perform, :around, :reset_batch_values
34
- set_callback :perform_unlocked, :after, :continue_discard_or_finish_batch
35
-
36
- # Parse a string representing a group of queues into a more readable data
37
- # structure.
38
- # @param string [String] Queue string
39
- # @return [Hash]
40
- # How to match a given queue. It can have the following keys and values:
41
- # - +{ all: true }+ indicates that all queues match.
42
- # - +{ exclude: Array<String> }+ indicates the listed queue names should
43
- # not match.
44
- # - +{ include: Array<String> }+ indicates the listed queue names should
45
- # match.
46
- # - +{ include: Array<String>, ordered_queues: true }+ indicates the listed
47
- # queue names should match, and dequeue should respect queue order.
48
- # @example
49
- # GoodJob::Execution.queue_parser('-queue1,queue2')
50
- # => { exclude: [ 'queue1', 'queue2' ] }
51
- def self.queue_parser(string)
52
- string = string.strip.presence || '*'
53
-
54
- case string.first
55
- when '-'
56
- exclude_queues = true
57
- string = string[1..]
58
- when '+'
59
- ordered_queues = true
60
- string = string[1..]
61
- end
62
-
63
- queues = string.split(',').map(&:strip)
64
-
65
- if queues.include?('*')
66
- { all: true }
67
- elsif exclude_queues
68
- { exclude: queues }
69
- elsif ordered_queues
70
- {
71
- include: queues,
72
- ordered_queues: true,
73
- }
74
- else
75
- { include: queues }
76
- end
77
- end
78
-
79
- # With a given class name
80
- # @!method job_class(name)
81
- # @!scope class
82
- # @param name [String] Job class name
83
- # @return [ActiveRecord::Relation]
84
- scope :job_class, ->(name) { where(params_job_class.eq(name)) }
85
-
86
- # Get jobs with given ActiveJob ID
87
- # @!method active_job_id(active_job_id)
88
- # @!scope class
89
- # @param active_job_id [String]
90
- # ActiveJob ID
91
- # @return [ActiveRecord::Relation]
92
- scope :active_job_id, ->(active_job_id) { where(active_job_id: active_job_id) }
93
-
94
- # Get jobs that have not yet finished (succeeded or discarded).
95
- # @!method unfinished
96
- # @!scope class
97
- # @return [ActiveRecord::Relation]
98
- scope :unfinished, -> { where(finished_at: nil) }
99
-
100
- # Get jobs that are not scheduled for a later time than now (i.e. jobs that
101
- # are not scheduled or scheduled for earlier than the current time).
102
- # @!method only_scheduled
103
- # @!scope class
104
- # @return [ActiveRecord::Relation]
105
- scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(bind_value('scheduled_at', DateTime.current, ActiveRecord::Type::DateTime))).or(where(scheduled_at: nil)) }
106
-
107
- # Order jobs by priority (highest priority first).
108
- # @!method priority_ordered
109
- # @!scope class
110
- # @return [ActiveRecord::Relation]
111
- scope :priority_ordered, (lambda do
112
- if GoodJob.configuration.smaller_number_is_higher_priority
113
- order('priority ASC NULLS LAST')
114
- else
115
- order('priority DESC NULLS LAST')
116
- end
117
- end)
118
-
119
- # Order jobs by created_at, for first-in first-out
120
- # @!method creation_ordered
121
- # @!scope class
122
- # @return [ActiveRecord:Relation]
123
- scope :creation_ordered, -> { order(created_at: :asc) }
124
-
125
- # Order jobs for de-queueing
126
- # @!method dequeueing_ordered(parsed_queues)
127
- # @!scope class
128
- # @param parsed_queues [Hash]
129
- # optional output of .queue_parser, parsed queues, will be used for
130
- # ordered queues.
131
- # @return [ActiveRecord::Relation]
132
- scope :dequeueing_ordered, (lambda do |parsed_queues|
133
- relation = self
134
- relation = relation.queue_ordered(parsed_queues[:include]) if parsed_queues && parsed_queues[:ordered_queues] && parsed_queues[:include]
135
- relation = relation.priority_ordered.creation_ordered
136
-
137
- relation
138
- end)
139
-
140
- # Order jobs in order of queues in array param
141
- # @!method queue_ordered(queues)
142
- # @!scope class
143
- # @param queues [Array<string] ordered names of queues
144
- # @return [ActiveRecord::Relation]
145
- scope :queue_ordered, (lambda do |queues|
146
- clauses = queues.map.with_index do |queue_name, index|
147
- "WHEN queue_name = '#{queue_name}' THEN #{index}"
148
- end
149
-
150
- order(
151
- Arel.sql("(CASE #{clauses.join(' ')} ELSE #{queues.length} END)")
152
- )
153
- end)
154
-
155
- # Order jobs by scheduled or created (oldest first).
156
- # @!method schedule_ordered
157
- # @!scope class
158
- # @return [ActiveRecord::Relation]
159
- scope :schedule_ordered, -> { order(coalesce_scheduled_at_created_at.asc) }
160
-
161
- # Get completed jobs before the given timestamp. If no timestamp is
162
- # provided, get *all* completed jobs. By default, GoodJob
163
- # destroys jobs after they're completed, meaning this returns no jobs.
164
- # However, if you have changed {GoodJob.preserve_job_records}, this may
165
- # find completed Jobs.
166
- # @!method finished(timestamp = nil)
167
- # @!scope class
168
- # @param timestamp (Float)
169
- # Get jobs that finished before this time (in epoch time).
170
- # @return [ActiveRecord::Relation]
171
- scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(bind_value('finished_at', timestamp, ActiveRecord::Type::DateTime))) : where.not(finished_at: nil) }
172
-
173
- # Get Jobs that started but not finished yet.
174
- # @!method running
175
- # @!scope class
176
- # @return [ActiveRecord::Relation]
177
- scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
178
-
179
- # Get Jobs on queues that match the given queue string.
180
- # @!method queue_string(string)
181
- # @!scope class
182
- # @param string [String]
183
- # A string expression describing what queues to select. See
184
- # {Job.queue_parser} or
185
- # {file:README.md#optimize-queues-threads-and-processes} for more details
186
- # on the format of the string. Note this only handles individual
187
- # semicolon-separated segments of that string format.
188
- # @return [ActiveRecord::Relation]
189
- scope :queue_string, (lambda do |string|
190
- parsed = queue_parser(string)
191
-
192
- if parsed[:all]
193
- all
194
- elsif parsed[:exclude]
195
- where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
196
- elsif parsed[:include]
197
- where(queue_name: parsed[:include])
198
- end
199
- end)
200
-
201
- class << self
202
- def json_string(json, attr)
203
- Arel::Nodes::Grouping.new(Arel::Nodes::InfixOperation.new('->>', json, Arel::Nodes.build_quoted(attr)))
204
- end
205
-
206
- def params_job_class
207
- json_string(arel_table['serialized_params'], 'job_class')
208
- end
209
-
210
- def params_execution_count
211
- Arel::Nodes::InfixOperation.new(
212
- '::',
213
- json_string(arel_table['serialized_params'], 'executions'),
214
- Arel.sql('integer')
215
- )
216
- end
217
-
218
- def coalesce_scheduled_at_created_at
219
- arel_table.coalesce(arel_table['scheduled_at'], arel_table['created_at'])
220
- end
221
- end
222
-
223
- # Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
224
- #
225
- # @param ignore_deserialization_errors [Boolean]
226
- # Whether to ignore ActiveJob::DeserializationError and NameError when deserializing the arguments.
227
- # This is most useful if you aren't planning to use the arguments directly.
228
- def active_job(ignore_deserialization_errors: false)
229
- ActiveJob::Base.deserialize(active_job_data).tap do |aj|
230
- aj.send(:deserialize_arguments_if_needed)
231
- rescue ActiveJob::DeserializationError
232
- raise unless ignore_deserialization_errors
233
- end
234
- rescue NameError
235
- raise unless ignore_deserialization_errors
236
- end
237
-
238
- def self.build_for_enqueue(active_job, scheduled_at: nil)
239
- new(**enqueue_args(active_job, scheduled_at: scheduled_at))
240
- end
241
-
242
- # Construct arguments for GoodJob::Execution from an ActiveJob instance.
243
- def self.enqueue_args(active_job, scheduled_at: nil)
244
- execution_args = {
245
- id: active_job.job_id,
246
- active_job_id: active_job.job_id,
247
- job_class: active_job.class.name,
248
- queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
249
- priority: active_job.priority || DEFAULT_PRIORITY,
250
- serialized_params: active_job.serialize,
251
- created_at: Time.current,
252
- }
253
-
254
- execution_args[:scheduled_at] = if scheduled_at
255
- scheduled_at
256
- elsif active_job.scheduled_at
257
- Time.zone.at(active_job.scheduled_at)
258
- else
259
- execution_args[:created_at]
260
- end
261
-
262
- execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
263
-
264
- if active_job.respond_to?(:good_job_labels) && active_job.good_job_labels.any?
265
- labels = active_job.good_job_labels.dup
266
- labels.map! { |label| label.to_s.strip.presence }
267
- labels.tap(&:compact!).tap(&:uniq!)
268
- execution_args[:labels] = labels
269
- end
270
-
271
- reenqueued_current_job = CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
272
- current_job = CurrentThread.job
273
-
274
- if reenqueued_current_job
275
- execution_args[:batch_id] = current_job.batch_id
276
- execution_args[:batch_callback_id] = current_job.batch_callback_id
277
- execution_args[:cron_key] = current_job.cron_key
278
- else
279
- execution_args[:batch_id] = GoodJob::Batch.current_batch_id
280
- execution_args[:batch_callback_id] = GoodJob::Batch.current_batch_callback_id
281
- execution_args[:cron_key] = CurrentThread.cron_key
282
- execution_args[:cron_at] = CurrentThread.cron_at
283
- end
284
-
285
- execution_args
286
- end
287
-
288
- # Finds the next eligible Execution, acquire an advisory lock related to it, and
289
- # executes the job.
290
- # @yield [Execution, nil] The next eligible Execution, or +nil+ if none found, before it is performed.
291
- # @return [ExecutionResult, nil]
292
- # If a job was executed, returns an array with the {Execution} record, the
293
- # return value for the job's +#perform+ method, and the exception the job
294
- # raised, if any (if the job raised, then the second array entry will be
295
- # +nil+). If there were no jobs to execute, returns +nil+.
296
- def self.perform_with_advisory_lock(lock_id:, parsed_queues: nil, queue_select_limit: nil)
297
- job = nil
298
- result = nil
299
-
300
- unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(select_limit: queue_select_limit) do |jobs|
301
- job = jobs.first
302
-
303
- if job&.executable?
304
- yield(job) if block_given?
305
-
306
- result = job.perform(lock_id: lock_id)
307
- else
308
- job = nil
309
- yield(nil) if block_given?
310
- end
311
- end
312
-
313
- job&.run_callbacks(:perform_unlocked)
314
- result
315
- end
316
-
317
- # Fetches the scheduled execution time of the next eligible Execution(s).
318
- # @param after [DateTime]
319
- # @param limit [Integer]
320
- # @param now_limit [Integer, nil]
321
- # @return [Array<DateTime>]
322
- def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
323
- query = advisory_unlocked.unfinished.schedule_ordered
324
-
325
- after ||= Time.current
326
- after_bind = bind_value('scheduled_at', after, ActiveRecord::Type::DateTime)
327
- 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))
328
- after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
329
-
330
- if now_limit&.positive?
331
- now_query = query.where(arel_table['scheduled_at'].lt(bind_value('scheduled_at', Time.current, ActiveRecord::Type::DateTime))).or query.where(scheduled_at: nil)
332
- now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
333
- end
334
-
335
- Array(now_at) + after_at
336
- end
337
-
338
- # Places an ActiveJob job on a queue by creating a new {Execution} record.
339
- # @param active_job [ActiveJob::Base]
340
- # The job to enqueue.
341
- # @param scheduled_at [Float]
342
- # Epoch timestamp when the job should be executed, if blank will delegate to the ActiveJob instance
343
- # @param create_with_advisory_lock [Boolean]
344
- # Whether to establish a lock on the {Execution} record after it is created.
345
- # @return [Execution]
346
- # The new {Execution} instance representing the queued ActiveJob job.
347
- def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
348
- 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|
349
- current_job = CurrentThread.job
350
-
351
- retried = current_job && current_job.active_job_id == active_job.job_id
352
- if retried
353
- job = current_job
354
- job.assign_attributes(enqueue_args(active_job, scheduled_at: scheduled_at))
355
- job.scheduled_at ||= Time.current
356
- # TODO: these values ideally shouldn't be persisted until the current_job is finished
357
- # which will require handling `retry_job` being called from outside the job context.
358
- job.performed_at = nil
359
- job.finished_at = nil
360
- else
361
- job = build_for_enqueue(active_job, scheduled_at: scheduled_at)
362
- end
363
-
364
- if create_with_advisory_lock
365
- if job.persisted?
366
- job.advisory_lock
367
- else
368
- job.create_with_advisory_lock = true
369
- end
370
- end
371
-
372
- instrument_payload[:job] = job
373
- job.save!
374
-
375
- CurrentThread.execution_retried = (job if retried)
376
-
377
- active_job.provider_job_id = job.id
378
- raise "These should be equal" if active_job.provider_job_id != active_job.job_id
379
-
380
- job
381
- end
382
- end
383
-
384
- def self.format_error(error)
385
- raise ArgumentError unless error.is_a?(Exception)
386
-
387
- [error.class.to_s, ERROR_MESSAGE_SEPARATOR, error.message].join
388
- end
389
-
390
- # Execute the ActiveJob job this {Execution} represents.
391
- # @return [ExecutionResult]
392
- # An array of the return value of the job's +#perform+ method and the
393
- # exception raised by the job, if any. If the job completed successfully,
394
- # the second array entry (the exception) will be +nil+ and vice versa.
395
- def perform(lock_id:)
396
- run_callbacks(:perform) do
397
- raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
398
-
399
- job_performed_at = Time.current
400
- monotonic_start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
401
- discrete_execution = nil
402
- result = GoodJob::CurrentThread.within do |current_thread|
403
- current_thread.reset
404
- current_thread.job = self
405
-
406
- existing_performed_at = performed_at
407
- if existing_performed_at
408
- current_thread.execution_interrupted = existing_performed_at
409
-
410
- interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{existing_performed_at}'"))
411
- self.error = interrupt_error_string
412
- self.error_event = :interrupted
413
- monotonic_duration = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - monotonic_start).seconds
414
-
415
- discrete_execution_attrs = {
416
- error: interrupt_error_string,
417
- finished_at: job_performed_at,
418
- error_event: :interrupted,
419
- duration: monotonic_duration,
420
- }
421
- discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
422
- end
423
-
424
- transaction do
425
- discrete_execution_attrs = {
426
- job_class: job_class,
427
- queue_name: queue_name,
428
- serialized_params: serialized_params,
429
- scheduled_at: (scheduled_at || created_at),
430
- created_at: job_performed_at,
431
- process_id: lock_id,
432
- }
433
- job_attrs = {
434
- performed_at: job_performed_at,
435
- executions_count: ((executions_count || 0) + 1),
436
- locked_by_id: lock_id,
437
- locked_at: Time.current,
438
- }
439
-
440
- discrete_execution = discrete_executions.create!(discrete_execution_attrs)
441
- update!(job_attrs)
442
- end
443
-
444
- ActiveSupport::Notifications.instrument("perform_job.good_job", { job: self, execution: discrete_execution, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do |instrument_payload|
445
- value = ActiveJob::Base.execute(active_job_data)
446
-
447
- if value.is_a?(Exception)
448
- handled_error = value
449
- value = nil
450
- end
451
- handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
452
-
453
- error_event = if handled_error == current_thread.error_on_discard
454
- :discarded
455
- elsif handled_error == current_thread.error_on_retry
456
- :retried
457
- elsif handled_error == current_thread.error_on_retry_stopped
458
- :retry_stopped
459
- elsif handled_error
460
- :handled
461
- end
462
-
463
- instrument_payload.merge!(
464
- value: value,
465
- handled_error: handled_error,
466
- retried: current_thread.execution_retried.present?,
467
- error_event: error_event
468
- )
469
- ExecutionResult.new(value: value, handled_error: handled_error, error_event: error_event, retried: current_thread.execution_retried)
470
- rescue StandardError => e
471
- error_event = if e.is_a?(GoodJob::InterruptError)
472
- :interrupted
473
- elsif e == current_thread.error_on_retry_stopped
474
- :retry_stopped
475
- else
476
- :unhandled
477
- end
478
-
479
- instrument_payload[:unhandled_error] = e
480
- ExecutionResult.new(value: nil, unhandled_error: e, error_event: error_event)
481
- end
482
- end
483
-
484
- job_attributes = { locked_by_id: nil, locked_at: nil }
485
-
486
- job_error = result.handled_error || result.unhandled_error
487
- if job_error
488
- error_string = self.class.format_error(job_error)
489
-
490
- job_attributes[:error] = error_string
491
- job_attributes[:error_event] = result.error_event
492
-
493
- discrete_execution.error = error_string
494
- discrete_execution.error_event = result.error_event
495
- discrete_execution.error_backtrace = job_error.backtrace
496
- else
497
- job_attributes[:error] = nil
498
- job_attributes[:error_event] = nil
499
- end
500
-
501
- job_finished_at = Time.current
502
- monotonic_duration = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - monotonic_start).seconds
503
- job_attributes[:finished_at] = job_finished_at
504
-
505
- discrete_execution.finished_at = job_finished_at
506
- discrete_execution.duration = monotonic_duration
507
-
508
- retry_unhandled_error = result.unhandled_error && GoodJob.retry_on_unhandled_error
509
- reenqueued = result.retried? || retried_good_job_id.present? || retry_unhandled_error
510
- if reenqueued
511
- job_attributes[:performed_at] = nil
512
- job_attributes[:finished_at] = nil
513
- end
514
-
515
- assign_attributes(job_attributes)
516
- preserve_unhandled = (result.unhandled_error && (GoodJob.retry_on_unhandled_error || GoodJob.preserve_job_records == :on_unhandled_error))
517
- if finished_at.blank? || GoodJob.preserve_job_records == true || reenqueued || preserve_unhandled || cron_key.present?
518
- transaction do
519
- discrete_execution.save!
520
- save!
521
- end
522
- else
523
- destroy!
524
- end
525
-
526
- result
527
- end
528
- end
529
-
530
- # Tests whether this job is safe to be executed by this thread.
531
- # @return [Boolean]
532
- def executable?
533
- reload.finished_at.blank?
534
- rescue ActiveRecord::RecordNotFound
535
- false
536
- end
537
-
538
- # Return formatted serialized_params for display in the dashboard
539
- # @return [Hash]
540
- def display_serialized_params
541
- serialized_params.merge({
542
- _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
543
- })
544
- end
545
-
546
- def running?
547
- if has_attribute?(:locktype)
548
- self['locktype'].present?
549
- else
550
- advisory_locked?
551
- end
552
- end
553
-
554
- def number
555
- serialized_params.fetch('executions', 0) + 1
556
- end
557
-
558
- # Time between when this job was expected to run and when it started running
559
- def queue_latency
560
- now = Time.zone.now
561
- expected_start = scheduled_at || created_at
562
- actual_start = performed_at || finished_at || now
563
-
564
- actual_start - expected_start unless expected_start >= now
565
- end
566
-
567
- # Time between when this job started and finished
568
- def runtime_latency
569
- (finished_at || Time.zone.now) - performed_at if performed_at
570
- end
571
-
572
- # Destroys this execution and all executions within the same job
573
- def destroy_job
574
- @_destroy_job = true
575
- destroy!
576
- ensure
577
- @_destroy_job = false
578
- end
579
-
580
- def job_state
581
- state = { queue_name: queue_name }
582
- state[:scheduled_at] = scheduled_at if scheduled_at
583
- state
584
- end
585
-
586
- private
587
-
588
- def reset_batch_values(&block)
589
- GoodJob::Batch.within_thread(batch_id: nil, batch_callback_id: nil, &block)
590
- end
591
-
592
- def continue_discard_or_finish_batch
593
- batch._continue_discard_or_finish(self) if batch.present?
594
- end
595
-
596
- def active_job_data
597
- serialized_params.deep_dup
598
- .tap do |job_data|
599
- job_data["provider_job_id"] = id
600
- job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
601
- job_data["good_job_labels"] = Array(labels) if labels.present?
602
- end
603
- end
604
- end
605
- end
@@ -1,5 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/arrow-clockwise/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
3
- <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
4
- <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
5
- </svg>
@@ -1,5 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/check-circle/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle <%= local_assigns[:class] %>" viewBox="0 0 16 16">
3
- <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
4
- <path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" />
5
- </svg>
@@ -1,4 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/circle-half/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle-half" viewBox="0 0 16 16">
3
- <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
4
- </svg>
@@ -1,5 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/clock/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clock" viewBox="0 0 16 16">
3
- <path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z" />
4
- <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z" />
5
- </svg>
@@ -1,5 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/dash-circle/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dash-circle" viewBox="0 0 16 16">
3
- <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
4
- <path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z" />
5
- </svg>
@@ -1,3 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
2
- <path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" />
3
- </svg>
@@ -1,4 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/eject/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eject" viewBox="0 0 16 16">
3
- <path d="M7.27 1.047a1 1 0 0 1 1.46 0l6.345 6.77c.6.638.146 1.683-.73 1.683H1.656C.78 9.5.326 8.455.926 7.816L7.27 1.047zM14.346 8.5 8 1.731 1.654 8.5h12.692zM.5 11.5a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1v-1zm14 0h-13v1h13v-1z" />
4
- </svg>
@@ -1,5 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/exclamation-circle/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-circle <%= local_assigns[:class] %>" viewBox="0 0 16 16">
3
- <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
4
- <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
5
- </svg>
@@ -1,3 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe <%= local_assigns[:class] %>" viewBox="0 0 16 16">
2
- <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z" />
3
- </svg>
@@ -1,4 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
2
- <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
3
- <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
4
- </svg>
@@ -1,5 +0,0 @@
1
- <!-- https://icons.getbootstrap.com/icons/moon-stars-fill/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-stars-fill" viewBox="0 0 16 16">
3
- <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
4
- <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
5
- </svg>