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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -25
  3. data/README.md +8 -6
  4. data/lib/active_job/arguments.rb +46 -47
  5. data/lib/active_job/base.rb +4 -6
  6. data/lib/active_job/configured_job.rb +5 -4
  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 +12 -2
  13. data/lib/active_job/enqueue_after_transaction_commit.rb +1 -26
  14. data/lib/active_job/enqueuing.rb +8 -4
  15. data/lib/active_job/exceptions.rb +16 -6
  16. data/lib/active_job/execution_state.rb +11 -0
  17. data/lib/active_job/gem_version.rb +3 -3
  18. data/lib/active_job/instrumentation.rb +12 -12
  19. data/lib/active_job/log_subscriber.rb +61 -6
  20. data/lib/active_job/queue_adapters/abstract_adapter.rb +6 -0
  21. data/lib/active_job/queue_adapters/async_adapter.rb +5 -1
  22. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +19 -0
  23. data/lib/active_job/queue_adapters/test_adapter.rb +5 -1
  24. data/lib/active_job/railtie.rb +9 -19
  25. data/lib/active_job/serializers/action_controller_parameters_serializer.rb +25 -0
  26. data/lib/active_job/serializers/big_decimal_serializer.rb +3 -4
  27. data/lib/active_job/serializers/date_serializer.rb +3 -4
  28. data/lib/active_job/serializers/date_time_serializer.rb +3 -4
  29. data/lib/active_job/serializers/duration_serializer.rb +5 -6
  30. data/lib/active_job/serializers/module_serializer.rb +3 -4
  31. data/lib/active_job/serializers/object_serializer.rb +11 -13
  32. data/lib/active_job/serializers/range_serializer.rb +9 -9
  33. data/lib/active_job/serializers/symbol_serializer.rb +4 -5
  34. data/lib/active_job/serializers/time_serializer.rb +3 -4
  35. data/lib/active_job/serializers/time_with_zone_serializer.rb +3 -4
  36. data/lib/active_job/serializers.rb +46 -15
  37. data/lib/active_job.rb +2 -0
  38. metadata +15 -11
  39. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -56
  40. data/lib/active_job/timezones.rb +0 -13
  41. 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"
@@ -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
@@ -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
- enqueue_after_transaction_commit = self.class.enqueue_after_transaction_commit
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
@@ -88,7 +88,7 @@ module ActiveJob
88
88
  end
89
89
 
90
90
  private
91
- def job_or_instantiate(*args, &_) # :doc:
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
- run_callbacks :enqueue do
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(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,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
 
@@ -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
@@ -8,9 +8,9 @@ module ActiveJob
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 8
11
- MINOR = 0
12
- TINY = 2
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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(ex.backtrace).join("\n")
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
- Thread.each_caller_location do |location|
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(DEFAULT_EXECUTOR_OPTIONS.merge(options))
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:)