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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -62
  3. data/README.md +7 -5
  4. data/lib/active_job/arguments.rb +51 -48
  5. data/lib/active_job/base.rb +3 -4
  6. data/lib/active_job/configured_job.rb +6 -1
  7. data/lib/active_job/continuable.rb +102 -0
  8. data/lib/active_job/continuation/step.rb +83 -0
  9. data/lib/active_job/continuation/test_helper.rb +89 -0
  10. data/lib/active_job/continuation/validation.rb +50 -0
  11. data/lib/active_job/continuation.rb +332 -0
  12. data/lib/active_job/core.rb +21 -3
  13. data/lib/active_job/enqueue_after_transaction_commit.rb +20 -10
  14. data/lib/active_job/enqueuing.rb +11 -8
  15. data/lib/active_job/exceptions.rb +17 -8
  16. data/lib/active_job/execution_state.rb +11 -0
  17. data/lib/active_job/gem_version.rb +2 -2
  18. data/lib/active_job/instrumentation.rb +12 -12
  19. data/lib/active_job/log_subscriber.rb +63 -4
  20. data/lib/active_job/queue_adapter.rb +1 -0
  21. data/lib/active_job/queue_adapters/abstract_adapter.rb +5 -7
  22. data/lib/active_job/queue_adapters/async_adapter.rb +6 -2
  23. data/lib/active_job/queue_adapters/delayed_job_adapter.rb +0 -8
  24. data/lib/active_job/queue_adapters/inline_adapter.rb +0 -4
  25. data/lib/active_job/queue_adapters/queue_classic_adapter.rb +0 -8
  26. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +19 -0
  27. data/lib/active_job/queue_adapters/test_adapter.rb +5 -9
  28. data/lib/active_job/queue_adapters.rb +0 -4
  29. data/lib/active_job/railtie.rb +15 -6
  30. data/lib/active_job/serializers/action_controller_parameters_serializer.rb +25 -0
  31. data/lib/active_job/serializers/big_decimal_serializer.rb +3 -4
  32. data/lib/active_job/serializers/date_serializer.rb +3 -4
  33. data/lib/active_job/serializers/date_time_serializer.rb +3 -4
  34. data/lib/active_job/serializers/duration_serializer.rb +5 -6
  35. data/lib/active_job/serializers/module_serializer.rb +3 -4
  36. data/lib/active_job/serializers/object_serializer.rb +11 -14
  37. data/lib/active_job/serializers/range_serializer.rb +9 -9
  38. data/lib/active_job/serializers/symbol_serializer.rb +4 -5
  39. data/lib/active_job/serializers/time_serializer.rb +3 -4
  40. data/lib/active_job/serializers/time_with_zone_serializer.rb +3 -4
  41. data/lib/active_job/serializers.rb +62 -18
  42. data/lib/active_job/structured_event_subscriber.rb +220 -0
  43. data/lib/active_job.rb +3 -12
  44. metadata +16 -11
  45. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -49
  46. data/lib/active_job/timezones.rb +0 -13
  47. 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"
@@ -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
- job = job_data["job_class"].constantize.new
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 = Time.iso8601(job_data["enqueued_at"]) if job_data["enqueued_at"]
161
- self.scheduled_at = Time.iso8601(job_data["scheduled_at"]) if job_data["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
- after_transaction = case self.class.enqueue_after_transaction_commit
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
@@ -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
- # - `:always` forces the job to be deferred.
52
- # - `:never` forces the job to be queued immediately.
53
- # - `:default` lets the queue adapter define the behavior (recommended).
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, &_) # :doc:
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
- run_callbacks :enqueue do
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(YetAnotherCustomAppException) do |job, error|
53
- # ExceptionNotifier.caught(error)
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(CustomAppException) do |job, error|
95
- # ExceptionNotifier.caught(error)
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
- job = dup
154
- job.enqueue options
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
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module ExecutionState # :nodoc:
5
+ def perform_now
6
+ I18n.with_locale(locale) do
7
+ Time.use_zone(timezone) { super }
8
+ end
9
+ end
10
+ end
11
+ end