activejob 8.0.2 → 8.1.0.beta1
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 +63 -25
- data/README.md +8 -6
- data/lib/active_job/arguments.rb +46 -47
- data/lib/active_job/base.rb +4 -6
- data/lib/active_job/configured_job.rb +5 -4
- data/lib/active_job/continuable.rb +102 -0
- data/lib/active_job/continuation/step.rb +83 -0
- data/lib/active_job/continuation/test_helper.rb +89 -0
- data/lib/active_job/continuation/validation.rb +50 -0
- data/lib/active_job/continuation.rb +332 -0
- data/lib/active_job/core.rb +12 -2
- data/lib/active_job/enqueue_after_transaction_commit.rb +1 -26
- data/lib/active_job/enqueuing.rb +8 -4
- data/lib/active_job/exceptions.rb +16 -6
- data/lib/active_job/execution_state.rb +11 -0
- data/lib/active_job/gem_version.rb +3 -3
- data/lib/active_job/instrumentation.rb +12 -12
- data/lib/active_job/log_subscriber.rb +61 -6
- data/lib/active_job/queue_adapters/abstract_adapter.rb +6 -0
- data/lib/active_job/queue_adapters/async_adapter.rb +5 -1
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +19 -0
- data/lib/active_job/queue_adapters/test_adapter.rb +5 -1
- data/lib/active_job/railtie.rb +9 -19
- data/lib/active_job/serializers/action_controller_parameters_serializer.rb +25 -0
- data/lib/active_job/serializers/big_decimal_serializer.rb +3 -4
- data/lib/active_job/serializers/date_serializer.rb +3 -4
- data/lib/active_job/serializers/date_time_serializer.rb +3 -4
- data/lib/active_job/serializers/duration_serializer.rb +5 -6
- data/lib/active_job/serializers/module_serializer.rb +3 -4
- data/lib/active_job/serializers/object_serializer.rb +11 -13
- data/lib/active_job/serializers/range_serializer.rb +9 -9
- data/lib/active_job/serializers/symbol_serializer.rb +4 -5
- data/lib/active_job/serializers/time_serializer.rb +3 -4
- data/lib/active_job/serializers/time_with_zone_serializer.rb +3 -4
- data/lib/active_job/serializers.rb +46 -15
- data/lib/active_job.rb +2 -0
- metadata +15 -11
- data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -56
- data/lib/active_job/timezones.rb +0 -13
- data/lib/active_job/translation.rb +0 -13
@@ -0,0 +1,332 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/numeric/time"
|
4
|
+
require "active_job/continuable"
|
5
|
+
|
6
|
+
module ActiveJob
|
7
|
+
# = Active Job \Continuation
|
8
|
+
#
|
9
|
+
# Continuations provide a mechanism for interrupting and resuming jobs. This allows
|
10
|
+
# long-running jobs to make progress across application restarts.
|
11
|
+
#
|
12
|
+
# Jobs should include the ActiveJob::Continuable module to enable continuations.
|
13
|
+
# \Continuable jobs are automatically retried when interrupted.
|
14
|
+
#
|
15
|
+
# Use the +step+ method to define the steps in your job. Steps can use an optional
|
16
|
+
# cursor to track progress in the step.
|
17
|
+
#
|
18
|
+
# Steps are executed as soon as they are encountered. If a job is interrupted, previously
|
19
|
+
# completed steps will be skipped. If a step is in progress, it will be resumed
|
20
|
+
# with the last recorded cursor.
|
21
|
+
#
|
22
|
+
# Code that is not part of a step will be executed on each job run.
|
23
|
+
#
|
24
|
+
# You can pass a block or a method name to the step method. The block will be called with
|
25
|
+
# the step object as an argument. Methods can either take no arguments or a single argument
|
26
|
+
# for the step object.
|
27
|
+
#
|
28
|
+
# class ProcessImportJob < ApplicationJob
|
29
|
+
# include ActiveJob::Continuable
|
30
|
+
#
|
31
|
+
# def perform(import_id)
|
32
|
+
# # This always runs, even if the job is resumed.
|
33
|
+
# @import = Import.find(import_id)
|
34
|
+
#
|
35
|
+
# step :validate do
|
36
|
+
# @import.validate!
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# step(:process_records) do |step|
|
40
|
+
# @import.records.find_each(start: step.cursor) do |record|
|
41
|
+
# record.process
|
42
|
+
# step.advance! from: record.id
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# step :reprocess_records
|
47
|
+
# step :finalize
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def reprocess_records(step)
|
51
|
+
# @import.records.find_each(start: step.cursor) do |record|
|
52
|
+
# record.reprocess
|
53
|
+
# step.advance! from: record.id
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# def finalize
|
58
|
+
# @import.finalize!
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# === Cursors
|
63
|
+
#
|
64
|
+
# Cursors are used to track progress within a step. The cursor can be any object that is
|
65
|
+
# serializable as an argument to +ActiveJob::Base.serialize+. It defaults to +nil+.
|
66
|
+
#
|
67
|
+
# When a step is resumed, the last cursor value is restored. The code in the step is responsible
|
68
|
+
# for using the cursor to continue from the right point.
|
69
|
+
#
|
70
|
+
# +set!+ sets the cursor to a specific value.
|
71
|
+
#
|
72
|
+
# step :iterate_items do |step|
|
73
|
+
# items[step.cursor..].each do |item|
|
74
|
+
# process(item)
|
75
|
+
# step.set! (step.cursor || 0) + 1
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# An starting value for the cursor can be set when defining the step:
|
80
|
+
#
|
81
|
+
# step :iterate_items, start: 0 do |step|
|
82
|
+
# items[step.cursor..].each do |item|
|
83
|
+
# process(item)
|
84
|
+
# step.set! step.cursor + 1
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# The cursor can be advanced with +advance!+. This calls +succ+ on the current cursor value.
|
89
|
+
# It raises an ActiveJob::Continuation::UnadvanceableCursorError if the cursor does not implement +succ+.
|
90
|
+
#
|
91
|
+
# step :iterate_items, start: 0 do |step|
|
92
|
+
# items[step.cursor..].each do |item|
|
93
|
+
# process(item)
|
94
|
+
# step.advance!
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# You can optionally pass a +from+ argument to +advance!+. This is useful when iterating
|
99
|
+
# over a collection of records where IDs may not be contiguous.
|
100
|
+
#
|
101
|
+
# step :process_records do |step|
|
102
|
+
# import.records.find_each(start: step.cursor) do |record|
|
103
|
+
# record.process
|
104
|
+
# step.advance! from: record.id
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# You can use an array to iterate over nested records:
|
109
|
+
#
|
110
|
+
# step :process_nested_records, start: [ 0, 0 ] do |step|
|
111
|
+
# Account.find_each(start: step.cursor[0]) do |account|
|
112
|
+
# account.records.find_each(start: step.cursor[1]) do |record|
|
113
|
+
# record.process
|
114
|
+
# step.set! [ account.id, record.id + 1 ]
|
115
|
+
# end
|
116
|
+
# step.set! [ account.id + 1, 0 ]
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# Setting or advancing the cursor creates a checkpoint. You can also create a checkpoint
|
121
|
+
# manually by calling the +checkpoint!+ method on the step. This is useful if you want to
|
122
|
+
# allow interruptions, but don't need to update the cursor.
|
123
|
+
#
|
124
|
+
# step :destroy_records do |step|
|
125
|
+
# import.records.find_each do |record|
|
126
|
+
# record.destroy!
|
127
|
+
# step.checkpoint!
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# === Checkpoints
|
132
|
+
#
|
133
|
+
# A checkpoint is where a job can be interrupted. At a checkpoint the job will call
|
134
|
+
# +queue_adapter.stopping?+. If it returns true, the job will raise an
|
135
|
+
# ActiveJob::Continuation::Interrupt exception.
|
136
|
+
#
|
137
|
+
# There is an automatic checkpoint before the start of each step except for the first for
|
138
|
+
# each job execution. Within a step one is created when calling +set!+, +advance!+ or +checkpoint!+.
|
139
|
+
#
|
140
|
+
# Jobs are not automatically interrupted when the queue adapter is marked as stopping - they
|
141
|
+
# will continue to run either until the next checkpoint, or when the process is stopped.
|
142
|
+
#
|
143
|
+
# This is to allow jobs to be interrupted at a safe point, but it also means that the jobs
|
144
|
+
# should checkpoint more frequently than the shutdown timeout to ensure a graceful restart.
|
145
|
+
#
|
146
|
+
# When interrupted, the job will automatically retry with the progress serialized
|
147
|
+
# in the job data under the +continuation+ key.
|
148
|
+
#
|
149
|
+
# The serialized progress contains:
|
150
|
+
# - a list of the completed steps
|
151
|
+
# - the current step and its cursor value (if one is in progress)
|
152
|
+
#
|
153
|
+
# === Isolated Steps
|
154
|
+
#
|
155
|
+
# Steps run sequentially in a single job execution, unless the job is interrupted.
|
156
|
+
#
|
157
|
+
# You can specify that a step should always run in its own execution by passing the +isolated: true+ option.
|
158
|
+
#
|
159
|
+
# This is useful for long-running steps where it may not be possible to checkpoint within
|
160
|
+
# the job grace period - it ensures that progress is serialized back into the job data before
|
161
|
+
# the step starts.
|
162
|
+
#
|
163
|
+
# step :quick_step1
|
164
|
+
# step :slow_step, isolated: true
|
165
|
+
# step :quick_step2
|
166
|
+
# step :quick_step3
|
167
|
+
#
|
168
|
+
# === Errors
|
169
|
+
#
|
170
|
+
# If a job raises an error and is not retried via Active Job, it will be passed back to the underlying
|
171
|
+
# queue backend and any progress in this execution will be lost.
|
172
|
+
#
|
173
|
+
# To mitigate this, the job will be automatically retried if it raises an error after it has made progress.
|
174
|
+
# Making progress is defined as having completed a step or advanced the cursor within the current step.
|
175
|
+
#
|
176
|
+
# === Configuration
|
177
|
+
#
|
178
|
+
# Continuable jobs have several configuration options:
|
179
|
+
# * <tt>:max_resumptions</tt> - The maximum number of times a job can be resumed. Defaults to +nil+ which means
|
180
|
+
# unlimited resumptions.
|
181
|
+
# * <tt>:resume_options</tt> - Options to pass to +retry_job+ when resuming the job.
|
182
|
+
# Defaults to <tt>{ wait: 5.seconds }</tt>.
|
183
|
+
# See {ActiveJob::Exceptions#retry_job}[rdoc-ref:ActiveJob::Exceptions#retry_job] for available options.
|
184
|
+
# * <tt>:resume_errors_after_advancing</tt> - Whether to resume errors after advancing the continuation.
|
185
|
+
# Defaults to +true+.
|
186
|
+
class Continuation
|
187
|
+
extend ActiveSupport::Autoload
|
188
|
+
|
189
|
+
autoload :Validation
|
190
|
+
|
191
|
+
# Raised when a job is interrupted, allowing Active Job to requeue it.
|
192
|
+
# This inherits from +Exception+ rather than +StandardError+, so it's not
|
193
|
+
# caught by normal exception handling.
|
194
|
+
class Interrupt < Exception; end
|
195
|
+
|
196
|
+
# Base class for all Continuation errors.
|
197
|
+
class Error < StandardError; end
|
198
|
+
|
199
|
+
# Raised when a step is invalid.
|
200
|
+
class InvalidStepError < Error; end
|
201
|
+
|
202
|
+
# Raised when there is an error with a checkpoint, such as open database transactions.
|
203
|
+
class CheckpointError < Error; end
|
204
|
+
|
205
|
+
# Raised when attempting to advance a cursor that doesn't implement `succ`.
|
206
|
+
class UnadvanceableCursorError < Error; end
|
207
|
+
|
208
|
+
# Raised when a job has reached its limit of the number of resumes.
|
209
|
+
# The limit is defined by the +max_resumes+ class attribute.
|
210
|
+
class ResumeLimitError < Error; end
|
211
|
+
|
212
|
+
include Validation
|
213
|
+
|
214
|
+
def initialize(job, serialized_progress) # :nodoc:
|
215
|
+
@job = job
|
216
|
+
@completed = serialized_progress.fetch("completed", []).map(&:to_sym)
|
217
|
+
@current = new_step(*serialized_progress["current"], resumed: true) if serialized_progress.key?("current")
|
218
|
+
@encountered = []
|
219
|
+
@advanced = false
|
220
|
+
@running_step = false
|
221
|
+
@isolating = false
|
222
|
+
end
|
223
|
+
|
224
|
+
def step(name, **options, &block) # :nodoc:
|
225
|
+
validate_step!(name)
|
226
|
+
encountered << name
|
227
|
+
|
228
|
+
if completed?(name)
|
229
|
+
skip_step(name)
|
230
|
+
else
|
231
|
+
run_step(name, **options, &block)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def to_h # :nodoc:
|
236
|
+
{
|
237
|
+
"completed" => completed.map(&:to_s),
|
238
|
+
"current" => current&.to_a,
|
239
|
+
}.compact
|
240
|
+
end
|
241
|
+
|
242
|
+
def description # :nodoc:
|
243
|
+
if current
|
244
|
+
current.description
|
245
|
+
elsif completed.any?
|
246
|
+
"after '#{completed.last}'"
|
247
|
+
else
|
248
|
+
"not started"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def started?
|
253
|
+
completed.any? || current.present?
|
254
|
+
end
|
255
|
+
|
256
|
+
def advanced?
|
257
|
+
@advanced
|
258
|
+
end
|
259
|
+
|
260
|
+
def instrumentation
|
261
|
+
{ description: description,
|
262
|
+
completed_steps: completed,
|
263
|
+
current_step: current }
|
264
|
+
end
|
265
|
+
|
266
|
+
private
|
267
|
+
attr_reader :job, :encountered, :completed, :current
|
268
|
+
|
269
|
+
def running_step?
|
270
|
+
@running_step
|
271
|
+
end
|
272
|
+
|
273
|
+
def isolating?
|
274
|
+
@isolating
|
275
|
+
end
|
276
|
+
|
277
|
+
def completed?(name)
|
278
|
+
completed.include?(name)
|
279
|
+
end
|
280
|
+
|
281
|
+
def new_step(*args, **options)
|
282
|
+
Step.new(*args, job: job, **options)
|
283
|
+
end
|
284
|
+
|
285
|
+
def skip_step(name)
|
286
|
+
instrument :step_skipped, step: name
|
287
|
+
end
|
288
|
+
|
289
|
+
def run_step(name, start:, isolated:, &block)
|
290
|
+
@isolating ||= isolated
|
291
|
+
|
292
|
+
if isolating? && advanced?
|
293
|
+
job.interrupt!(reason: :isolating)
|
294
|
+
else
|
295
|
+
run_step_inline(name, start: start, &block)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def run_step_inline(name, start:, **options, &block)
|
300
|
+
@running_step = true
|
301
|
+
@current ||= new_step(name, start, resumed: false)
|
302
|
+
|
303
|
+
instrumenting_step(current) do
|
304
|
+
block.call(current)
|
305
|
+
end
|
306
|
+
|
307
|
+
@completed << current.name
|
308
|
+
@current = nil
|
309
|
+
@advanced = true
|
310
|
+
ensure
|
311
|
+
@running_step = false
|
312
|
+
@advanced ||= current&.advanced?
|
313
|
+
end
|
314
|
+
|
315
|
+
def instrumenting_step(step, &block)
|
316
|
+
instrument :step, step: step, interrupted: false do |payload|
|
317
|
+
instrument :step_started, step: step
|
318
|
+
|
319
|
+
block.call
|
320
|
+
rescue Interrupt
|
321
|
+
payload[:interrupted] = true
|
322
|
+
raise
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def instrument(...)
|
327
|
+
job.instrument(...)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
require "active_job/continuation/step"
|
data/lib/active_job/core.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveJob
|
4
|
+
# Raised during job payload deserialization when it references an uninitialized job class.
|
5
|
+
class UnknownJobClassError < NameError
|
6
|
+
def initialize(job_class_name)
|
7
|
+
super("Failed to instantiate job, class `#{job_class_name}` doesn't exist", job_class_name)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
4
11
|
# = Active Job \Core
|
5
12
|
#
|
6
13
|
# Provides general behavior that will be included into every Active Job
|
@@ -60,7 +67,10 @@ module ActiveJob
|
|
60
67
|
module ClassMethods
|
61
68
|
# Creates a new job instance from a hash created with +serialize+
|
62
69
|
def deserialize(job_data)
|
63
|
-
|
70
|
+
job_class = job_data["job_class"].safe_constantize
|
71
|
+
raise UnknownJobClassError, job_data["job_class"] unless job_class
|
72
|
+
|
73
|
+
job = job_class.new
|
64
74
|
job.deserialize(job_data)
|
65
75
|
job
|
66
76
|
end
|
@@ -114,7 +124,7 @@ module ActiveJob
|
|
114
124
|
"arguments" => serialize_arguments_if_needed(arguments),
|
115
125
|
"executions" => executions,
|
116
126
|
"exception_executions" => exception_executions,
|
117
|
-
"locale" => I18n.locale.to_s,
|
127
|
+
"locale" => locale || I18n.locale.to_s,
|
118
128
|
"timezone" => timezone,
|
119
129
|
"enqueued_at" => Time.now.utc.iso8601(9),
|
120
130
|
"scheduled_at" => scheduled_at ? scheduled_at.utc.iso8601(9) : nil,
|
@@ -4,32 +4,7 @@ module ActiveJob
|
|
4
4
|
module EnqueueAfterTransactionCommit # :nodoc:
|
5
5
|
private
|
6
6
|
def raw_enqueue
|
7
|
-
|
8
|
-
|
9
|
-
after_transaction = case self.class.enqueue_after_transaction_commit
|
10
|
-
when :always
|
11
|
-
ActiveJob.deprecator.warn(<<~MSG.squish)
|
12
|
-
Setting `#{self.class.name}.enqueue_after_transaction_commit = :always` is deprecated and will be removed in Rails 8.1.
|
13
|
-
Set to `true` to always enqueue the job after the transaction is committed.
|
14
|
-
MSG
|
15
|
-
true
|
16
|
-
when :never
|
17
|
-
ActiveJob.deprecator.warn(<<~MSG.squish)
|
18
|
-
Setting `#{self.class.name}.enqueue_after_transaction_commit = :never` is deprecated and will be removed in Rails 8.1.
|
19
|
-
Set to `false` to never enqueue the job after the transaction is committed.
|
20
|
-
MSG
|
21
|
-
false
|
22
|
-
when :default
|
23
|
-
ActiveJob.deprecator.warn(<<~MSG.squish)
|
24
|
-
Setting `#{self.class.name}.enqueue_after_transaction_commit = :default` is deprecated and will be removed in Rails 8.1.
|
25
|
-
Set to `false` to never enqueue the job after the transaction is committed.
|
26
|
-
MSG
|
27
|
-
false
|
28
|
-
else
|
29
|
-
enqueue_after_transaction_commit
|
30
|
-
end
|
31
|
-
|
32
|
-
if after_transaction
|
7
|
+
if self.class.enqueue_after_transaction_commit
|
33
8
|
self.successfully_enqueued = true
|
34
9
|
ActiveRecord.after_all_transactions_commit do
|
35
10
|
self.successfully_enqueued = false
|
data/lib/active_job/enqueuing.rb
CHANGED
@@ -88,7 +88,7 @@ module ActiveJob
|
|
88
88
|
end
|
89
89
|
|
90
90
|
private
|
91
|
-
def job_or_instantiate(*args, &
|
91
|
+
def job_or_instantiate(*args, &) # :doc:
|
92
92
|
args.first.is_a?(self) ? args.first : new(*args)
|
93
93
|
end
|
94
94
|
ruby2_keywords(:job_or_instantiate)
|
@@ -113,9 +113,7 @@ module ActiveJob
|
|
113
113
|
set(options)
|
114
114
|
self.successfully_enqueued = false
|
115
115
|
|
116
|
-
|
117
|
-
raw_enqueue
|
118
|
-
end
|
116
|
+
raw_enqueue
|
119
117
|
|
120
118
|
if successfully_enqueued?
|
121
119
|
self
|
@@ -126,6 +124,12 @@ module ActiveJob
|
|
126
124
|
|
127
125
|
private
|
128
126
|
def raw_enqueue
|
127
|
+
run_callbacks :enqueue do
|
128
|
+
_raw_enqueue
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def _raw_enqueue
|
129
133
|
if scheduled_at
|
130
134
|
queue_adapter.enqueue_at self, scheduled_at.to_f
|
131
135
|
else
|
@@ -34,6 +34,7 @@ module ActiveJob
|
|
34
34
|
# * <tt>:queue</tt> - Re-enqueues the job on a different queue
|
35
35
|
# * <tt>:priority</tt> - Re-enqueues the job with a different priority
|
36
36
|
# * <tt>:jitter</tt> - A random delay of wait time used when calculating backoff. The default is 15% (0.15) which represents the upper bound of possible wait time (expressed as a percentage)
|
37
|
+
# * <tt>:report</tt> - Errors will be reported to the Rails.error reporter before being retried
|
37
38
|
#
|
38
39
|
# ==== Examples
|
39
40
|
#
|
@@ -49,8 +50,9 @@ module ActiveJob
|
|
49
50
|
# # retry_on Net::ReadTimeout, wait: 5.seconds, jitter: 0.30, attempts: 10
|
50
51
|
# # retry_on Timeout::Error, wait: :polynomially_longer, attempts: 10
|
51
52
|
#
|
52
|
-
# retry_on
|
53
|
-
#
|
53
|
+
# retry_on YetAnotherCustomAppException, report: true
|
54
|
+
# retry_on EvenWorseCustomAppException do |job, error|
|
55
|
+
# CustomErrorHandlingCode.handle(job, error)
|
54
56
|
# end
|
55
57
|
#
|
56
58
|
# def perform(*args)
|
@@ -59,10 +61,11 @@ module ActiveJob
|
|
59
61
|
# # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
|
60
62
|
# end
|
61
63
|
# end
|
62
|
-
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT)
|
64
|
+
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT, report: false)
|
63
65
|
rescue_from(*exceptions) do |error|
|
64
66
|
executions = executions_for(exceptions)
|
65
67
|
if attempts == :unlimited || executions < attempts
|
68
|
+
ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
|
66
69
|
retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error
|
67
70
|
else
|
68
71
|
if block_given?
|
@@ -82,6 +85,8 @@ module ActiveJob
|
|
82
85
|
# Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
|
83
86
|
# like an Active Record, is no longer available, and the job is thus no longer relevant.
|
84
87
|
#
|
88
|
+
# Passing the <tt>:report</tt> option reports the error through the error reporter before discarding the job.
|
89
|
+
#
|
85
90
|
# You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.
|
86
91
|
#
|
87
92
|
# +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
|
@@ -91,8 +96,9 @@ module ActiveJob
|
|
91
96
|
#
|
92
97
|
# class SearchIndexingJob < ActiveJob::Base
|
93
98
|
# discard_on ActiveJob::DeserializationError
|
94
|
-
# discard_on
|
95
|
-
#
|
99
|
+
# discard_on CustomAppException, report: true
|
100
|
+
# discard_on(AnotherCustomAppException) do |job, error|
|
101
|
+
# CustomErrorHandlingCode.handle(job, error)
|
96
102
|
# end
|
97
103
|
#
|
98
104
|
# def perform(record)
|
@@ -100,9 +106,10 @@ module ActiveJob
|
|
100
106
|
# # Might raise CustomAppException for something domain specific
|
101
107
|
# end
|
102
108
|
# end
|
103
|
-
def discard_on(*exceptions)
|
109
|
+
def discard_on(*exceptions, report: false)
|
104
110
|
rescue_from(*exceptions) do |error|
|
105
111
|
instrument :discard, error: error do
|
112
|
+
ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
|
106
113
|
yield self, error if block_given?
|
107
114
|
run_after_discard_procs(error)
|
108
115
|
end
|
@@ -150,7 +157,10 @@ module ActiveJob
|
|
150
157
|
# end
|
151
158
|
def retry_job(options = {})
|
152
159
|
instrument :enqueue_retry, options.slice(:error, :wait) do
|
160
|
+
scheduled_at, queue_name, priority = self.scheduled_at, self.queue_name, self.priority
|
153
161
|
enqueue options
|
162
|
+
ensure
|
163
|
+
self.scheduled_at, self.queue_name, self.priority = scheduled_at, queue_name, priority
|
154
164
|
end
|
155
165
|
end
|
156
166
|
|
@@ -26,24 +26,24 @@ module ActiveJob
|
|
26
26
|
instrument(:perform) { super }
|
27
27
|
end
|
28
28
|
|
29
|
+
def instrument(operation, payload = {}, &block) # :nodoc:
|
30
|
+
payload[:job] = self
|
31
|
+
payload[:adapter] = queue_adapter
|
32
|
+
|
33
|
+
ActiveSupport::Notifications.instrument("#{operation}.active_job", payload) do |payload|
|
34
|
+
value = block.call(payload) if block
|
35
|
+
payload[:aborted] = @_halted_callback_hook_called if defined?(@_halted_callback_hook_called)
|
36
|
+
@_halted_callback_hook_called = nil
|
37
|
+
value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
29
41
|
private
|
30
42
|
def _perform_job
|
31
43
|
instrument(:perform_start)
|
32
44
|
super
|
33
45
|
end
|
34
46
|
|
35
|
-
def instrument(operation, payload = {}, &block)
|
36
|
-
payload[:job] = self
|
37
|
-
payload[:adapter] = queue_adapter
|
38
|
-
|
39
|
-
ActiveSupport::Notifications.instrument("#{operation}.active_job", payload) do
|
40
|
-
value = block.call if block
|
41
|
-
payload[:aborted] = @_halted_callback_hook_called if defined?(@_halted_callback_hook_called)
|
42
|
-
@_halted_callback_hook_called = nil
|
43
|
-
value
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
47
|
def halted_callback_hook(*)
|
48
48
|
super
|
49
49
|
@_halted_callback_hook_called = true
|
@@ -87,8 +87,9 @@ module ActiveJob
|
|
87
87
|
job = event.payload[:job]
|
88
88
|
ex = event.payload[:exception_object]
|
89
89
|
if ex
|
90
|
+
cleaned_backtrace = backtrace_cleaner.clean(ex.backtrace)
|
90
91
|
error do
|
91
|
-
"Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(
|
92
|
+
"Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(cleaned_backtrace).join("\n")
|
92
93
|
end
|
93
94
|
elsif event.payload[:aborted]
|
94
95
|
error do
|
@@ -137,6 +138,64 @@ module ActiveJob
|
|
137
138
|
end
|
138
139
|
subscribe_log_level :discard, :error
|
139
140
|
|
141
|
+
def interrupt(event)
|
142
|
+
job = event.payload[:job]
|
143
|
+
info do
|
144
|
+
"Interrupted #{job.class} (Job ID: #{job.job_id}) #{event.payload[:description]} (#{event.payload[:reason]})"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
subscribe_log_level :interrupt, :info
|
148
|
+
|
149
|
+
def resume(event)
|
150
|
+
job = event.payload[:job]
|
151
|
+
info do
|
152
|
+
"Resuming #{job.class} (Job ID: #{job.job_id}) #{event.payload[:description]}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
subscribe_log_level :resume, :info
|
156
|
+
|
157
|
+
def step_skipped(event)
|
158
|
+
job = event.payload[:job]
|
159
|
+
info do
|
160
|
+
"Step '#{event.payload[:step].name}' skipped #{job.class}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
subscribe_log_level :step_skipped, :info
|
164
|
+
|
165
|
+
def step_started(event)
|
166
|
+
job = event.payload[:job]
|
167
|
+
step = event.payload[:step]
|
168
|
+
info do
|
169
|
+
if step.resumed?
|
170
|
+
"Step '#{step.name}' resumed from cursor '#{step.cursor}' for #{job.class} (Job ID: #{job.job_id})"
|
171
|
+
else
|
172
|
+
"Step '#{step.name}' started for #{job.class} (Job ID: #{job.job_id})"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
subscribe_log_level :step_started, :info
|
177
|
+
|
178
|
+
def step(event)
|
179
|
+
job = event.payload[:job]
|
180
|
+
step = event.payload[:step]
|
181
|
+
ex = event.payload[:exception_object]
|
182
|
+
|
183
|
+
if event.payload[:interrupted]
|
184
|
+
info do
|
185
|
+
"Step '#{step.name}' interrupted at cursor '#{step.cursor}' for #{job.class} (Job ID: #{job.job_id}) in #{event.duration.round(2)}ms"
|
186
|
+
end
|
187
|
+
elsif ex
|
188
|
+
error do
|
189
|
+
"Error during step '#{step.name}' at cursor '#{step.cursor}' for #{job.class} (Job ID: #{job.job_id}) in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message})"
|
190
|
+
end
|
191
|
+
else
|
192
|
+
info do
|
193
|
+
"Step '#{step.name}' completed for #{job.class} (Job ID: #{job.job_id}) in #{event.duration.round(2)}ms"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
subscribe_log_level :step, :error
|
198
|
+
|
140
199
|
private
|
141
200
|
def queue_name(event)
|
142
201
|
ActiveJob.adapter_name(event.payload[:adapter]) + "(#{event.payload[:job].queue_name})"
|
@@ -197,11 +256,7 @@ module ActiveJob
|
|
197
256
|
end
|
198
257
|
|
199
258
|
def enqueue_source_location
|
200
|
-
|
201
|
-
frame = backtrace_cleaner.clean_frame(location)
|
202
|
-
return frame if frame
|
203
|
-
end
|
204
|
-
nil
|
259
|
+
backtrace_cleaner.first_clean_frame
|
205
260
|
end
|
206
261
|
|
207
262
|
def enqueued_jobs_message(adapter, enqueued_jobs)
|
@@ -7,6 +7,8 @@ module ActiveJob
|
|
7
7
|
# Active Job supports multiple job queue systems. ActiveJob::QueueAdapters::AbstractAdapter
|
8
8
|
# forms the abstraction layer which makes this possible.
|
9
9
|
class AbstractAdapter
|
10
|
+
attr_accessor :stopping
|
11
|
+
|
10
12
|
def enqueue(job)
|
11
13
|
raise NotImplementedError
|
12
14
|
end
|
@@ -14,6 +16,10 @@ module ActiveJob
|
|
14
16
|
def enqueue_at(job, timestamp)
|
15
17
|
raise NotImplementedError
|
16
18
|
end
|
19
|
+
|
20
|
+
def stopping?
|
21
|
+
!!@stopping
|
22
|
+
end
|
17
23
|
end
|
18
24
|
end
|
19
25
|
end
|
@@ -86,7 +86,11 @@ module ActiveJob
|
|
86
86
|
def initialize(**options)
|
87
87
|
self.immediate = false
|
88
88
|
@immediate_executor = Concurrent::ImmediateExecutor.new
|
89
|
-
@async_executor = Concurrent::ThreadPoolExecutor.new(
|
89
|
+
@async_executor = Concurrent::ThreadPoolExecutor.new(
|
90
|
+
name: "ActiveJob-async-scheduler",
|
91
|
+
**DEFAULT_EXECUTOR_OPTIONS,
|
92
|
+
**options
|
93
|
+
)
|
90
94
|
end
|
91
95
|
|
92
96
|
def enqueue(job, queue_name:)
|