activejob 7.2.3 → 8.1.3
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 +93 -62
- data/README.md +7 -5
- data/lib/active_job/arguments.rb +51 -48
- data/lib/active_job/base.rb +3 -4
- data/lib/active_job/configured_job.rb +6 -1
- 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 +21 -3
- data/lib/active_job/enqueue_after_transaction_commit.rb +20 -10
- data/lib/active_job/enqueuing.rb +11 -8
- data/lib/active_job/exceptions.rb +17 -8
- data/lib/active_job/execution_state.rb +11 -0
- data/lib/active_job/gem_version.rb +2 -2
- data/lib/active_job/instrumentation.rb +12 -12
- data/lib/active_job/log_subscriber.rb +63 -4
- data/lib/active_job/queue_adapter.rb +1 -0
- data/lib/active_job/queue_adapters/abstract_adapter.rb +5 -7
- data/lib/active_job/queue_adapters/async_adapter.rb +6 -2
- data/lib/active_job/queue_adapters/delayed_job_adapter.rb +0 -8
- data/lib/active_job/queue_adapters/inline_adapter.rb +0 -4
- data/lib/active_job/queue_adapters/queue_classic_adapter.rb +0 -8
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +19 -0
- data/lib/active_job/queue_adapters/test_adapter.rb +5 -9
- data/lib/active_job/queue_adapters.rb +0 -4
- data/lib/active_job/railtie.rb +15 -6
- 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 -14
- 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 +62 -18
- data/lib/active_job/structured_event_subscriber.rb +220 -0
- data/lib/active_job.rb +3 -12
- metadata +16 -11
- data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -49
- data/lib/active_job/timezones.rb +0 -13
- data/lib/active_job/translation.rb +0 -13
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job/test_helper"
|
|
4
|
+
require "active_job/continuation"
|
|
5
|
+
|
|
6
|
+
module ActiveJob
|
|
7
|
+
class Continuation
|
|
8
|
+
# Test helper for ActiveJob::Continuable jobs.
|
|
9
|
+
#
|
|
10
|
+
module TestHelper
|
|
11
|
+
include ::ActiveJob::TestHelper
|
|
12
|
+
|
|
13
|
+
# Interrupt a job during a step.
|
|
14
|
+
#
|
|
15
|
+
# class MyJob < ApplicationJob
|
|
16
|
+
# include ActiveJob::Continuable
|
|
17
|
+
#
|
|
18
|
+
# cattr_accessor :items, default: []
|
|
19
|
+
# def perform
|
|
20
|
+
# step :my_step, start: 1 do |step|
|
|
21
|
+
# (step.cursor..10).each do |i|
|
|
22
|
+
# items << i
|
|
23
|
+
# step.advance!
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# test "interrupt job during step" do
|
|
30
|
+
# MyJob.perform_later
|
|
31
|
+
# interrupt_job_during_step(MyJob, :my_step, cursor: 6) { perform_enqueued_jobs }
|
|
32
|
+
# assert_equal [1, 2, 3, 4, 5], MyJob.items
|
|
33
|
+
# perform_enqueued_jobs
|
|
34
|
+
# assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], MyJob.items
|
|
35
|
+
# end
|
|
36
|
+
def interrupt_job_during_step(job, step, cursor: nil, &block)
|
|
37
|
+
require_active_job_test_adapter!("interrupt_job_during_step")
|
|
38
|
+
queue_adapter.with(stopping: ->() { during_step?(job, step, cursor: cursor) }, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Interrupt a job after a step.
|
|
42
|
+
#
|
|
43
|
+
# Note that there's no checkpoint after the final step so it won't be interrupted.
|
|
44
|
+
#
|
|
45
|
+
# class MyJob < ApplicationJob
|
|
46
|
+
# include ActiveJob::Continuable
|
|
47
|
+
#
|
|
48
|
+
# cattr_accessor :items, default: []
|
|
49
|
+
#
|
|
50
|
+
# def perform
|
|
51
|
+
# step :step_one { items << 1 }
|
|
52
|
+
# step :step_two { items << 2 }
|
|
53
|
+
# step :step_three { items << 3 }
|
|
54
|
+
# step :step_four { items << 4 }
|
|
55
|
+
# end
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# test "interrupt job after step" do
|
|
59
|
+
# MyJob.perform_later
|
|
60
|
+
# interrupt_job_after_step(MyJob, :step_two) { perform_enqueued_jobs }
|
|
61
|
+
# assert_equal [1, 2], MyJob.items
|
|
62
|
+
# perform_enqueued_jobs
|
|
63
|
+
# assert_equal [1, 2, 3, 4], MyJob.items
|
|
64
|
+
# end
|
|
65
|
+
def interrupt_job_after_step(job, step, &block)
|
|
66
|
+
require_active_job_test_adapter!("interrupt_job_after_step")
|
|
67
|
+
queue_adapter.with(stopping: ->() { after_step?(job, step) }, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
def continuation_for(klass)
|
|
72
|
+
job = ActiveSupport::ExecutionContext.to_h[:job]
|
|
73
|
+
job.send(:continuation)&.to_h if job && job.is_a?(klass)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def during_step?(job, step, cursor: nil)
|
|
77
|
+
if (continuation = continuation_for(job))
|
|
78
|
+
continuation["current"] == [ step.to_s, cursor ]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def after_step?(job, step)
|
|
83
|
+
if (continuation = continuation_for(job))
|
|
84
|
+
continuation["completed"].last == step.to_s && continuation["current"].nil?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
class Continuation
|
|
5
|
+
module Validation # :nodoc:
|
|
6
|
+
private
|
|
7
|
+
def validate_step!(name)
|
|
8
|
+
validate_step_symbol!(name)
|
|
9
|
+
validate_step_not_encountered!(name)
|
|
10
|
+
validate_step_not_nested!(name)
|
|
11
|
+
validate_step_resume_expected!(name)
|
|
12
|
+
validate_step_expected_order!(name)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate_step_symbol!(name)
|
|
16
|
+
unless name.is_a?(Symbol)
|
|
17
|
+
raise_step_error! "Step '#{name}' must be a Symbol, found '#{name.class}'"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate_step_not_encountered!(name)
|
|
22
|
+
if encountered.include?(name)
|
|
23
|
+
raise_step_error! "Step '#{name}' has already been encountered"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_step_not_nested!(name)
|
|
28
|
+
if running_step?
|
|
29
|
+
raise_step_error! "Step '#{name}' is nested inside step '#{current.name}'"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_step_resume_expected!(name)
|
|
34
|
+
if current && current.name != name && !completed?(name)
|
|
35
|
+
raise_step_error! "Step '#{name}' found, expected to resume from '#{current.name}'"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_step_expected_order!(name)
|
|
40
|
+
if completed.size > encountered.size && completed[encountered.size] != name
|
|
41
|
+
raise_step_error! "Step '#{name}' found, expected to see '#{completed[encountered.size]}'"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def raise_step_error!(message)
|
|
46
|
+
raise InvalidStepError, message
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -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
|
|
@@ -157,8 +167,8 @@ module ActiveJob
|
|
|
157
167
|
self.exception_executions = job_data["exception_executions"]
|
|
158
168
|
self.locale = job_data["locale"] || I18n.locale.to_s
|
|
159
169
|
self.timezone = job_data["timezone"] || Time.zone&.name
|
|
160
|
-
self.enqueued_at =
|
|
161
|
-
self.scheduled_at =
|
|
170
|
+
self.enqueued_at = deserialize_time(job_data["enqueued_at"]) if job_data["enqueued_at"]
|
|
171
|
+
self.scheduled_at = deserialize_time(job_data["scheduled_at"]) if job_data["scheduled_at"]
|
|
162
172
|
end
|
|
163
173
|
|
|
164
174
|
# Configures the job with the given options.
|
|
@@ -198,5 +208,13 @@ module ActiveJob
|
|
|
198
208
|
def arguments_serialized?
|
|
199
209
|
@serialized_arguments
|
|
200
210
|
end
|
|
211
|
+
|
|
212
|
+
def deserialize_time(time)
|
|
213
|
+
if time.is_a?(Time)
|
|
214
|
+
time
|
|
215
|
+
else
|
|
216
|
+
Time.iso8601(time)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
201
219
|
end
|
|
202
220
|
end
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveJob
|
|
4
4
|
module EnqueueAfterTransactionCommit # :nodoc:
|
|
5
|
+
class << self
|
|
6
|
+
def included(base)
|
|
7
|
+
ActiveJob.singleton_class.prepend(ActiveJobMethods)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ActiveJobMethods
|
|
12
|
+
# Ensures perform_all_later respects each job's enqueue_after_transaction_commit configuration.
|
|
13
|
+
# Jobs with enqueue_after_transaction_commit set to true are deferred and enqueued only after the transaction commits;
|
|
14
|
+
# other jobs are enqueued immediately. This ensures enqueuing timing matches the per-job setting.
|
|
15
|
+
def perform_all_later(*jobs)
|
|
16
|
+
jobs.flatten!
|
|
17
|
+
deferred_jobs, immediate_jobs = jobs.partition { |job| job.class.enqueue_after_transaction_commit }
|
|
18
|
+
super(immediate_jobs) if immediate_jobs.any?
|
|
19
|
+
ActiveRecord.after_all_transactions_commit { super(deferred_jobs) } if deferred_jobs.any?
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
5
24
|
private
|
|
6
25
|
def raw_enqueue
|
|
7
|
-
|
|
8
|
-
when :always
|
|
9
|
-
true
|
|
10
|
-
when :never
|
|
11
|
-
false
|
|
12
|
-
else # :default
|
|
13
|
-
queue_adapter.enqueue_after_transaction_commit?
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
if after_transaction
|
|
26
|
+
if self.class.enqueue_after_transaction_commit
|
|
17
27
|
self.successfully_enqueued = true
|
|
18
28
|
ActiveRecord.after_all_transactions_commit do
|
|
19
29
|
self.successfully_enqueued = false
|
data/lib/active_job/enqueuing.rb
CHANGED
|
@@ -48,10 +48,9 @@ module ActiveJob
|
|
|
48
48
|
# automatically defers the enqueue to after the transaction commits.
|
|
49
49
|
#
|
|
50
50
|
# It can be set on a per job basis:
|
|
51
|
-
# -
|
|
52
|
-
# -
|
|
53
|
-
|
|
54
|
-
class_attribute :enqueue_after_transaction_commit, instance_accessor: false, instance_predicate: false, default: :never
|
|
51
|
+
# - true forces the job to be deferred.
|
|
52
|
+
# - false forces the job to be queued immediately.
|
|
53
|
+
class_attribute :enqueue_after_transaction_commit, instance_accessor: false, instance_predicate: false, default: false
|
|
55
54
|
end
|
|
56
55
|
|
|
57
56
|
# Includes the +perform_later+ method for job initialization.
|
|
@@ -89,7 +88,7 @@ module ActiveJob
|
|
|
89
88
|
end
|
|
90
89
|
|
|
91
90
|
private
|
|
92
|
-
def job_or_instantiate(*args, &
|
|
91
|
+
def job_or_instantiate(*args, &) # :doc:
|
|
93
92
|
args.first.is_a?(self) ? args.first : new(*args)
|
|
94
93
|
end
|
|
95
94
|
ruby2_keywords(:job_or_instantiate)
|
|
@@ -114,9 +113,7 @@ module ActiveJob
|
|
|
114
113
|
set(options)
|
|
115
114
|
self.successfully_enqueued = false
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
raw_enqueue
|
|
119
|
-
end
|
|
116
|
+
raw_enqueue
|
|
120
117
|
|
|
121
118
|
if successfully_enqueued?
|
|
122
119
|
self
|
|
@@ -127,6 +124,12 @@ module ActiveJob
|
|
|
127
124
|
|
|
128
125
|
private
|
|
129
126
|
def raw_enqueue
|
|
127
|
+
run_callbacks :enqueue do
|
|
128
|
+
_raw_enqueue
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def _raw_enqueue
|
|
130
133
|
if scheduled_at
|
|
131
134
|
queue_adapter.enqueue_at self, scheduled_at.to_f
|
|
132
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,8 +157,10 @@ module ActiveJob
|
|
|
150
157
|
# end
|
|
151
158
|
def retry_job(options = {})
|
|
152
159
|
instrument :enqueue_retry, options.slice(:error, :wait) do
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
scheduled_at, queue_name, priority = self.scheduled_at, self.queue_name, self.priority
|
|
161
|
+
enqueue options
|
|
162
|
+
ensure
|
|
163
|
+
self.scheduled_at, self.queue_name, self.priority = scheduled_at, queue_name, priority
|
|
155
164
|
end
|
|
156
165
|
end
|
|
157
166
|
|