good_job 4.1.0 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/app/charts/good_job/performance_index_chart.rb +1 -1
  4. data/app/charts/good_job/performance_show_chart.rb +1 -1
  5. data/app/controllers/good_job/application_controller.rb +1 -1
  6. data/app/controllers/good_job/frontends_controller.rb +6 -2
  7. data/app/controllers/good_job/performance_controller.rb +1 -1
  8. data/app/frontend/good_job/icons.svg +79 -0
  9. data/app/frontend/good_job/style.css +5 -0
  10. data/app/helpers/good_job/icons_helper.rb +8 -5
  11. data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
  12. data/app/models/concerns/good_job/reportable.rb +8 -12
  13. data/app/models/good_job/batch.rb +10 -5
  14. data/app/models/good_job/batch_record.rb +18 -15
  15. data/app/models/good_job/discrete_execution.rb +6 -59
  16. data/app/models/good_job/execution.rb +59 -4
  17. data/app/models/good_job/execution_result.rb +6 -6
  18. data/app/models/good_job/job.rb +567 -12
  19. data/app/views/good_job/batches/_jobs.erb +1 -1
  20. data/app/views/good_job/batches/_table.erb +1 -1
  21. data/app/views/good_job/jobs/index.html.erb +1 -1
  22. data/app/views/layouts/good_job/application.html.erb +7 -7
  23. data/config/brakeman.ignore +75 -0
  24. data/config/locales/de.yml +49 -49
  25. data/config/locales/es.yml +14 -14
  26. data/config/routes.rb +3 -3
  27. data/lib/good_job/active_job_extensions/concurrency.rb +105 -98
  28. data/lib/good_job/adapter/inline_buffer.rb +73 -0
  29. data/lib/good_job/adapter.rb +59 -53
  30. data/lib/good_job/configuration.rb +3 -4
  31. data/lib/good_job/cron_manager.rb +1 -3
  32. data/lib/good_job/current_thread.rb +4 -4
  33. data/lib/good_job/version.rb +1 -1
  34. data/lib/good_job.rb +6 -5
  35. metadata +6 -20
  36. data/app/models/good_job/base_execution.rb +0 -605
  37. data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
  38. data/app/views/good_job/shared/icons/_check.html.erb +0 -5
  39. data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
  40. data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
  41. data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
  42. data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
  43. data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
  44. data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
  45. data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
  46. data/app/views/good_job/shared/icons/_info.html.erb +0 -4
  47. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
  48. data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
  49. data/app/views/good_job/shared/icons/_play.html.erb +0 -4
  50. data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
  51. data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
  52. 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>