@@ -134,8 +134,8 @@ module ActiveJob
134
134
# +queue_adapter.stopping?+. If it returns true, the job will raise an
135
135
# ActiveJob::Continuation::Interrupt exception.
136
136
#
137
- # There is an automatic checkpoint at the end of each step. Within a step one is
138
- # created when calling +set!+, +advance!+ or +checkpoint!+.
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
139
#
140
140
# Jobs are not automatically interrupted when the queue adapter is marked as stopping - they
141
141
# will continue to run either until the next checkpoint, or when the process is stopped.
@@ -158,49 +158,57 @@ module ActiveJob
158
158
# To mitigate this, the job will be automatically retried if it raises an error after it has made progress.
159
159
# Making progress is defined as having completed a step or advanced the cursor within the current step.
160
160
#
161
+ # === Configuration
162
+ #
163
+ # Continuable jobs have several configuration options:
164
+ # * :max_resumptions</tt> - The maximum number of times a job can be resumed. Defaults to +nil+ which means
165
+ # unlimited resumptions.
166
+ # * <tt>:resume_options</tt> - Options to pass to +retry_job+ when resuming the job.
167
+ # Defaults to +{ wait: 5.seconds }+.
168
+ # See +ActiveJob::Exceptions#retry_job+ for available options.
169
+ # * <tt>:resume_errors_after_advancing</tt> - Whether to resume errors after advancing the continuation.
170
+ # Defaults to +true+.
161
171
class Continuation
162
172
extend ActiveSupport ::Autoload
163
173
164
174
autoload :Step
175
+ autoload :Validation
165
176
166
177
# Raised when a job is interrupted, allowing Active Job to requeue it.
167
178
# This inherits from +Exception+ rather than +StandardError+, so it's not
168
179
# caught by normal exception handling.
169
180
class Interrupt < Exception ; end
170
181
171
- # Base error class for all Continuation errors.
182
+ # Base class for all Continuation errors.
172
183
class Error < StandardError ; end
173
184
174
185
# Raised when a step is invalid.
175
186
class InvalidStepError < Error ; end
176
187
188
+ # Raised when there is an error with a checkpoint, such as open database transactions.
189
+ class CheckpointError < Error ; end
190
+
177
191
# Raised when attempting to advance a cursor that doesn't implement `succ`.
178
192
class UnadvanceableCursorError < Error ; end
179
193
180
- # Raised when an error occurs after a job has made progress.
181
- #
182
- # The job will be automatically retried to ensure that the progress is serialized
183
- # in the retried job.
184
- class AfterAdvancingError < Error ; end
194
+ # Raised when a job has reached its limit of the number of resumes.
195
+ # The limit is defined by the +max_resumes+ class attribute.
196
+ class ResumeLimitError < Error ; end
185
197
186
- def initialize ( job , serialized_progress )
198
+ include Validation
199
+
200
+ def initialize ( job , serialized_progress ) # :nodoc:
187
201
@job = job
188
202
@completed = serialized_progress . fetch ( "completed" , [ ] ) . map ( &:to_sym )
189
203
@current = new_step ( *serialized_progress [ "current" ] , resumed : true ) if serialized_progress . key? ( "current" )
190
- @encountered_step_names = [ ]
204
+ @encountered = [ ]
191
205
@advanced = false
192
206
@running_step = false
193
207
end
194
208
195
- def continue ( &block )
196
- wrapping_errors_after_advancing do
197
- instrument_job :resume if started?
198
- block . call
199
- end
200
- end
201
-
202
- def step ( name , start :, &block )
209
+ def step ( name , start :, &block ) # :nodoc:
203
210
validate_step! ( name )
211
+ encountered << name
204
212
205
213
if completed? ( name )
206
214
skip_step ( name )
@@ -209,14 +217,14 @@ def step(name, start:, &block)
209
217
end
210
218
end
211
219
212
- def to_h
220
+ def to_h # :nodoc:
213
221
{
214
222
"completed" => completed . map ( &:to_s ) ,
215
223
"current" => current &.to_a
216
224
} . compact
217
225
end
218
226
219
- def description
227
+ def description # :nodoc:
220
228
if current
221
229
current . description
222
230
elsif completed . any?
@@ -226,36 +234,33 @@ def description
226
234
end
227
235
end
228
236
229
- private
230
- attr_reader :job , :encountered_step_names , :completed , :current
237
+ def started?
238
+ completed . any? || current . present?
239
+ end
231
240
232
- def advanced?
233
- @advanced
234
- end
241
+ def advanced?
242
+ @advanced
243
+ end
244
+
245
+ def instrumentation
246
+ { description : description ,
247
+ completed_steps : completed ,
248
+ current_step : current }
249
+ end
250
+
251
+ private
252
+ attr_reader :job , :encountered , :completed , :current
235
253
236
254
def running_step?
237
255
@running_step
238
256
end
239
257
240
- def started?
241
- completed . any? || current . present?
242
- end
243
-
244
258
def completed? ( name )
245
259
completed . include? ( name )
246
260
end
247
261
248
- def validate_step! ( name )
249
- raise InvalidStepError , "Step '#{ name } ' must be a Symbol, found '#{ name . class } '" unless name . is_a? ( Symbol )
250
- raise InvalidStepError , "Step '#{ name } ' has already been encountered" if encountered_step_names . include? ( name )
251
- raise InvalidStepError , "Step '#{ name } ' is nested inside step '#{ current . name } '" if running_step?
252
- raise InvalidStepError , "Step '#{ name } ' found, expected to resume from '#{ current . name } '" if current && current . name != name && !completed? ( name )
253
-
254
- encountered_step_names << name
255
- end
256
-
257
262
def new_step ( *args , **options )
258
- Step . new ( *args , **options ) { checkpoint! }
263
+ Step . new ( *args , job : job , **options )
259
264
end
260
265
261
266
def skip_step ( name )
@@ -273,49 +278,24 @@ def run_step(name, start:, &block)
273
278
@completed << current . name
274
279
@current = nil
275
280
@advanced = true
276
-
277
- checkpoint!
278
281
ensure
279
282
@running_step = false
280
283
@advanced ||= current &.advanced?
281
284
end
282
285
283
- def interrupt!
284
- instrument_job :interrupt
285
- raise Interrupt , "Interrupted #{ description } "
286
- end
287
-
288
- def checkpoint!
289
- interrupt! if job . queue_adapter . stopping?
290
- end
286
+ def instrumenting_step ( step , &block )
287
+ instrument :step , step : step , interrupted : false do |payload |
288
+ instrument :step_started , step : step
291
289
292
- def wrapping_errors_after_advancing ( &block )
293
- block . call
294
- rescue StandardError => e
295
- if !e . is_a? ( Error ) && advanced?
296
- raise AfterAdvancingError , "Advanced job failed with error: #{ e . message } "
297
- else
290
+ block . call
291
+ rescue Interrupt
292
+ payload [ :interrupted ] = true
298
293
raise
299
294
end
300
295
end
301
296
302
- def instrumenting_step ( step , &block )
303
- instrument ( step . resumed? ? :step_resumed : :step_started ) , step : step
304
-
305
- block . call
306
-
307
- instrument :step_completed , step : step
308
- rescue Interrupt
309
- instrument :step_interrupted , step : step
310
- raise
311
- end
312
-
313
- def instrument_job ( event )
314
- instrument event , description : description , completed_steps : completed , current_step : current
315
- end
316
-
317
- def instrument ( event , payload = { } )
318
- job . instrument event , **payload
297
+ def instrument ( ...)
298
+ job . instrument ( ...)
319
299
end
320
300
end
321
301
end
0 commit comments