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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/README.md +10 -10
- data/app/charts/good_job/performance_index_chart.rb +1 -1
- data/app/charts/good_job/performance_show_chart.rb +1 -1
- data/app/controllers/good_job/application_controller.rb +1 -1
- data/app/controllers/good_job/batches_controller.rb +6 -0
- data/app/controllers/good_job/frontends_controller.rb +6 -2
- data/app/controllers/good_job/performance_controller.rb +1 -1
- data/app/frontend/good_job/icons.svg +79 -0
- data/app/frontend/good_job/style.css +5 -0
- data/app/helpers/good_job/icons_helper.rb +8 -5
- data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
- data/app/models/concerns/good_job/error_events.rb +2 -2
- data/app/models/concerns/good_job/reportable.rb +8 -12
- data/app/models/good_job/batch.rb +31 -9
- data/app/models/good_job/batch_record.rb +19 -20
- data/app/models/good_job/discrete_execution.rb +6 -59
- data/app/models/good_job/execution.rb +59 -4
- data/app/models/good_job/execution_result.rb +6 -6
- data/app/models/good_job/job.rb +543 -12
- data/app/models/good_job/process.rb +14 -3
- data/app/views/good_job/batches/_jobs.erb +1 -1
- data/app/views/good_job/batches/_table.erb +7 -1
- data/app/views/good_job/batches/show.html.erb +8 -0
- data/app/views/good_job/jobs/index.html.erb +1 -1
- data/app/views/layouts/good_job/application.html.erb +7 -7
- data/config/brakeman.ignore +75 -0
- data/config/locales/de.yml +54 -49
- data/config/locales/en.yml +5 -0
- data/config/locales/es.yml +19 -14
- data/config/locales/fr.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/config/locales/ja.yml +10 -5
- data/config/locales/ko.yml +9 -4
- data/config/locales/nl.yml +5 -0
- data/config/locales/pt-BR.yml +5 -0
- data/config/locales/ru.yml +5 -0
- data/config/locales/tr.yml +5 -0
- data/config/locales/uk.yml +6 -1
- data/config/routes.rb +8 -4
- data/lib/good_job/active_job_extensions/concurrency.rb +109 -98
- data/lib/good_job/adapter/inline_buffer.rb +73 -0
- data/lib/good_job/adapter.rb +59 -53
- data/lib/good_job/capsule_tracker.rb +2 -2
- data/lib/good_job/configuration.rb +13 -12
- data/lib/good_job/cron_manager.rb +1 -3
- data/lib/good_job/current_thread.rb +4 -4
- data/lib/good_job/notifier/process_heartbeat.rb +3 -2
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +6 -5
- metadata +6 -20
- data/app/models/good_job/base_execution.rb +0 -605
- data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
- data/app/views/good_job/shared/icons/_check.html.erb +0 -5
- data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
- data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
- data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
- data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
- data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
- data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
- data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
- data/app/views/good_job/shared/icons/_info.html.erb +0 -4
- data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
- data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
- data/app/views/good_job/shared/icons/_play.html.erb +0 -4
- data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
- data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
- 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,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>
|