activejob 7.1.5.2 → 8.1.2.1
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 +86 -264
- data/README.md +8 -6
- data/lib/active_job/arguments.rb +51 -59
- data/lib/active_job/base.rb +5 -6
- data/lib/active_job/callbacks.rb +0 -3
- data/lib/active_job/configured_job.rb +5 -4
- data/lib/active_job/continuable.rb +102 -0
- data/lib/active_job/continuation/step.rb +83 -0
- data/lib/active_job/continuation/test_helper.rb +89 -0
- data/lib/active_job/continuation/validation.rb +50 -0
- data/lib/active_job/continuation.rb +332 -0
- data/lib/active_job/core.rb +25 -22
- data/lib/active_job/enqueue_after_transaction_commit.rb +38 -0
- data/lib/active_job/enqueuing.rb +46 -12
- data/lib/active_job/exceptions.rb +21 -17
- data/lib/active_job/execution_state.rb +11 -0
- data/lib/active_job/gem_version.rb +3 -3
- data/lib/active_job/instrumentation.rb +12 -12
- data/lib/active_job/log_subscriber.rb +65 -6
- data/lib/active_job/logging.rb +16 -2
- data/lib/active_job/queue_adapter.rb +6 -4
- data/lib/active_job/queue_adapters/abstract_adapter.rb +25 -0
- data/lib/active_job/queue_adapters/async_adapter.rb +7 -3
- data/lib/active_job/queue_adapters/backburner_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/delayed_job_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/queue_classic_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/resque_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +20 -1
- data/lib/active_job/queue_adapters/sneakers_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/test_adapter.rb +6 -2
- data/lib/active_job/queue_adapters.rb +1 -4
- data/lib/active_job/railtie.rb +25 -4
- 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 +13 -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/test_helper.rb +28 -5
- data/lib/active_job.rb +5 -9
- metadata +18 -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
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
|
|
@@ -13,9 +20,7 @@ module ActiveJob
|
|
|
13
20
|
attr_writer :serialized_arguments
|
|
14
21
|
|
|
15
22
|
# Time when the job should be performed
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
attr_reader :_scheduled_at_time # :nodoc:
|
|
23
|
+
attr_accessor :scheduled_at
|
|
19
24
|
|
|
20
25
|
# Job Identifier
|
|
21
26
|
attr_accessor :job_id
|
|
@@ -62,7 +67,10 @@ module ActiveJob
|
|
|
62
67
|
module ClassMethods
|
|
63
68
|
# Creates a new job instance from a hash created with +serialize+
|
|
64
69
|
def deserialize(job_data)
|
|
65
|
-
|
|
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
|
|
66
74
|
job.deserialize(job_data)
|
|
67
75
|
job
|
|
68
76
|
end
|
|
@@ -97,7 +105,6 @@ module ActiveJob
|
|
|
97
105
|
@job_id = SecureRandom.uuid
|
|
98
106
|
@queue_name = self.class.queue_name
|
|
99
107
|
@scheduled_at = nil
|
|
100
|
-
@_scheduled_at_time = nil
|
|
101
108
|
@priority = self.class.priority
|
|
102
109
|
@executions = 0
|
|
103
110
|
@exception_executions = {}
|
|
@@ -117,10 +124,10 @@ module ActiveJob
|
|
|
117
124
|
"arguments" => serialize_arguments_if_needed(arguments),
|
|
118
125
|
"executions" => executions,
|
|
119
126
|
"exception_executions" => exception_executions,
|
|
120
|
-
"locale" => I18n.locale.to_s,
|
|
127
|
+
"locale" => locale || I18n.locale.to_s,
|
|
121
128
|
"timezone" => timezone,
|
|
122
129
|
"enqueued_at" => Time.now.utc.iso8601(9),
|
|
123
|
-
"scheduled_at" =>
|
|
130
|
+
"scheduled_at" => scheduled_at ? scheduled_at.utc.iso8601(9) : nil,
|
|
124
131
|
}
|
|
125
132
|
end
|
|
126
133
|
|
|
@@ -160,8 +167,8 @@ module ActiveJob
|
|
|
160
167
|
self.exception_executions = job_data["exception_executions"]
|
|
161
168
|
self.locale = job_data["locale"] || I18n.locale.to_s
|
|
162
169
|
self.timezone = job_data["timezone"] || Time.zone&.name
|
|
163
|
-
self.enqueued_at =
|
|
164
|
-
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"]
|
|
165
172
|
end
|
|
166
173
|
|
|
167
174
|
# Configures the job with the given options.
|
|
@@ -174,18 +181,6 @@ module ActiveJob
|
|
|
174
181
|
self
|
|
175
182
|
end
|
|
176
183
|
|
|
177
|
-
def scheduled_at=(value)
|
|
178
|
-
@_scheduled_at_time = if value&.is_a?(Numeric)
|
|
179
|
-
ActiveJob.deprecator.warn(<<~MSG.squish)
|
|
180
|
-
Assigning a numeric/epoch value to scheduled_at is deprecated. Use a Time object instead.
|
|
181
|
-
MSG
|
|
182
|
-
Time.at(value)
|
|
183
|
-
else
|
|
184
|
-
value
|
|
185
|
-
end
|
|
186
|
-
@scheduled_at = value
|
|
187
|
-
end
|
|
188
|
-
|
|
189
184
|
private
|
|
190
185
|
def serialize_arguments_if_needed(arguments)
|
|
191
186
|
if arguments_serialized?
|
|
@@ -211,7 +206,15 @@ module ActiveJob
|
|
|
211
206
|
end
|
|
212
207
|
|
|
213
208
|
def arguments_serialized?
|
|
214
|
-
|
|
209
|
+
@serialized_arguments
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def deserialize_time(time)
|
|
213
|
+
if time.is_a?(Time)
|
|
214
|
+
time
|
|
215
|
+
else
|
|
216
|
+
Time.iso8601(time)
|
|
217
|
+
end
|
|
215
218
|
end
|
|
216
219
|
end
|
|
217
220
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
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
|
+
|
|
24
|
+
private
|
|
25
|
+
def raw_enqueue
|
|
26
|
+
if self.class.enqueue_after_transaction_commit
|
|
27
|
+
self.successfully_enqueued = true
|
|
28
|
+
ActiveRecord.after_all_transactions_commit do
|
|
29
|
+
self.successfully_enqueued = false
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
self
|
|
33
|
+
else
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/active_job/enqueuing.rb
CHANGED
|
@@ -21,7 +21,7 @@ module ActiveJob
|
|
|
21
21
|
adapter_jobs.each do |job|
|
|
22
22
|
job.successfully_enqueued = false
|
|
23
23
|
if job.scheduled_at
|
|
24
|
-
queue_adapter.enqueue_at(job, job.
|
|
24
|
+
queue_adapter.enqueue_at(job, job.scheduled_at.to_f)
|
|
25
25
|
else
|
|
26
26
|
queue_adapter.enqueue(job)
|
|
27
27
|
end
|
|
@@ -40,6 +40,19 @@ module ActiveJob
|
|
|
40
40
|
module Enqueuing
|
|
41
41
|
extend ActiveSupport::Concern
|
|
42
42
|
|
|
43
|
+
included do
|
|
44
|
+
##
|
|
45
|
+
# :singleton-method:
|
|
46
|
+
#
|
|
47
|
+
# Defines if enqueueing this job from inside an Active Record transaction
|
|
48
|
+
# automatically defers the enqueue to after the transaction commits.
|
|
49
|
+
#
|
|
50
|
+
# It can be set on a per job basis:
|
|
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
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
# Includes the +perform_later+ method for job initialization.
|
|
44
57
|
module ClassMethods
|
|
45
58
|
# Push a job onto the queue. By default the arguments must be either String,
|
|
@@ -50,9 +63,21 @@ module ActiveJob
|
|
|
50
63
|
# custom serializers.
|
|
51
64
|
#
|
|
52
65
|
# Returns an instance of the job class queued with arguments available in
|
|
53
|
-
# Job#arguments or false if the enqueue did not succeed.
|
|
66
|
+
# Job#arguments or +false+ if the enqueue did not succeed.
|
|
54
67
|
#
|
|
55
68
|
# After the attempted enqueue, the job will be yielded to an optional block.
|
|
69
|
+
#
|
|
70
|
+
# If Active Job is used conjointly with Active Record, and #perform_later is called
|
|
71
|
+
# inside an Active Record transaction, then the enqueue is implicitly deferred to after
|
|
72
|
+
# the transaction is committed, or dropped if it's rolled back. In such case #perform_later
|
|
73
|
+
# will return the job instance like if it was successfully enqueued, but will still return
|
|
74
|
+
# +false+ if a callback prevented the job from being enqueued.
|
|
75
|
+
#
|
|
76
|
+
# This behavior can be changed on a per job basis:
|
|
77
|
+
#
|
|
78
|
+
# class NotificationJob < ApplicationJob
|
|
79
|
+
# self.enqueue_after_transaction_commit = false
|
|
80
|
+
# end
|
|
56
81
|
def perform_later(...)
|
|
57
82
|
job = job_or_instantiate(...)
|
|
58
83
|
enqueue_result = job.enqueue
|
|
@@ -63,7 +88,7 @@ module ActiveJob
|
|
|
63
88
|
end
|
|
64
89
|
|
|
65
90
|
private
|
|
66
|
-
def job_or_instantiate(*args) # :doc:
|
|
91
|
+
def job_or_instantiate(*args, &) # :doc:
|
|
67
92
|
args.first.is_a?(self) ? args.first : new(*args)
|
|
68
93
|
end
|
|
69
94
|
ruby2_keywords(:job_or_instantiate)
|
|
@@ -88,9 +113,25 @@ module ActiveJob
|
|
|
88
113
|
set(options)
|
|
89
114
|
self.successfully_enqueued = false
|
|
90
115
|
|
|
91
|
-
|
|
116
|
+
raw_enqueue
|
|
117
|
+
|
|
118
|
+
if successfully_enqueued?
|
|
119
|
+
self
|
|
120
|
+
else
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
def raw_enqueue
|
|
127
|
+
run_callbacks :enqueue do
|
|
128
|
+
_raw_enqueue
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def _raw_enqueue
|
|
92
133
|
if scheduled_at
|
|
93
|
-
queue_adapter.enqueue_at self,
|
|
134
|
+
queue_adapter.enqueue_at self, scheduled_at.to_f
|
|
94
135
|
else
|
|
95
136
|
queue_adapter.enqueue self
|
|
96
137
|
end
|
|
@@ -99,12 +140,5 @@ module ActiveJob
|
|
|
99
140
|
rescue EnqueueError => e
|
|
100
141
|
self.enqueue_error = e
|
|
101
142
|
end
|
|
102
|
-
|
|
103
|
-
if successfully_enqueued?
|
|
104
|
-
self
|
|
105
|
-
else
|
|
106
|
-
false
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
143
|
end
|
|
110
144
|
end
|
|
@@ -21,7 +21,7 @@ module ActiveJob
|
|
|
21
21
|
# You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting
|
|
22
22
|
# the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
24
|
+
# +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
|
|
25
25
|
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if any.
|
|
26
26
|
#
|
|
27
27
|
# ==== Options
|
|
@@ -29,11 +29,12 @@ module ActiveJob
|
|
|
29
29
|
# as a computing proc that takes the number of executions so far as an argument, or as a symbol reference of
|
|
30
30
|
# <tt>:polynomially_longer</tt>, which applies the wait algorithm of <tt>((executions**4) + (Kernel.rand * (executions**4) * jitter)) + 2</tt>
|
|
31
31
|
# (first wait ~3s, then ~18s, then ~83s, etc)
|
|
32
|
-
# * <tt>:attempts</tt> -
|
|
33
|
-
# to retry the job until it succeeds
|
|
32
|
+
# * <tt>:attempts</tt> - Enqueues the job the specified number of times (default: 5 attempts) or a symbol reference of <tt>:unlimited</tt>
|
|
33
|
+
# to retry the job until it succeeds. The number of attempts includes the original job execution.
|
|
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,16 +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)
|
|
63
|
-
if wait == :exponentially_longer
|
|
64
|
-
ActiveJob.deprecator.warn(<<~MSG.squish)
|
|
65
|
-
`wait: :exponentially_longer` will actually wait polynomially longer and is therefore deprecated.
|
|
66
|
-
Prefer `wait: :polynomially_longer` to avoid confusion and keep the same behavior.
|
|
67
|
-
MSG
|
|
68
|
-
end
|
|
64
|
+
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT, report: false)
|
|
69
65
|
rescue_from(*exceptions) do |error|
|
|
70
66
|
executions = executions_for(exceptions)
|
|
71
67
|
if attempts == :unlimited || executions < attempts
|
|
68
|
+
ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
|
|
72
69
|
retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error
|
|
73
70
|
else
|
|
74
71
|
if block_given?
|
|
@@ -88,17 +85,20 @@ module ActiveJob
|
|
|
88
85
|
# Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
|
|
89
86
|
# like an Active Record, is no longer available, and the job is thus no longer relevant.
|
|
90
87
|
#
|
|
88
|
+
# Passing the <tt>:report</tt> option reports the error through the error reporter before discarding the job.
|
|
89
|
+
#
|
|
91
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.
|
|
92
91
|
#
|
|
93
|
-
#
|
|
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
|
|
94
93
|
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if any.
|
|
95
94
|
#
|
|
96
95
|
# ==== Example
|
|
97
96
|
#
|
|
98
97
|
# class SearchIndexingJob < ActiveJob::Base
|
|
99
98
|
# discard_on ActiveJob::DeserializationError
|
|
100
|
-
# discard_on
|
|
101
|
-
#
|
|
99
|
+
# discard_on CustomAppException, report: true
|
|
100
|
+
# discard_on(AnotherCustomAppException) do |job, error|
|
|
101
|
+
# CustomErrorHandlingCode.handle(job, error)
|
|
102
102
|
# end
|
|
103
103
|
#
|
|
104
104
|
# def perform(record)
|
|
@@ -106,9 +106,10 @@ module ActiveJob
|
|
|
106
106
|
# # Might raise CustomAppException for something domain specific
|
|
107
107
|
# end
|
|
108
108
|
# end
|
|
109
|
-
def discard_on(*exceptions)
|
|
109
|
+
def discard_on(*exceptions, report: false)
|
|
110
110
|
rescue_from(*exceptions) do |error|
|
|
111
111
|
instrument :discard, error: error do
|
|
112
|
+
ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
|
|
112
113
|
yield self, error if block_given?
|
|
113
114
|
run_after_discard_procs(error)
|
|
114
115
|
end
|
|
@@ -156,7 +157,10 @@ module ActiveJob
|
|
|
156
157
|
# end
|
|
157
158
|
def retry_job(options = {})
|
|
158
159
|
instrument :enqueue_retry, options.slice(:error, :wait) do
|
|
160
|
+
scheduled_at, queue_name, priority = self.scheduled_at, self.queue_name, self.priority
|
|
159
161
|
enqueue options
|
|
162
|
+
ensure
|
|
163
|
+
self.scheduled_at, self.queue_name, self.priority = scheduled_at, queue_name, priority
|
|
160
164
|
end
|
|
161
165
|
end
|
|
162
166
|
|
|
@@ -168,7 +172,7 @@ module ActiveJob
|
|
|
168
172
|
jitter = jitter == JITTER_DEFAULT ? self.class.retry_jitter : (jitter || 0.0)
|
|
169
173
|
|
|
170
174
|
case seconds_or_duration_or_algorithm
|
|
171
|
-
when
|
|
175
|
+
when :polynomially_longer
|
|
172
176
|
# This delay uses a polynomial backoff strategy, which was previously misnamed as exponential
|
|
173
177
|
delay = executions**4
|
|
174
178
|
delay_jitter = determine_jitter_for_delay(delay, jitter)
|
|
@@ -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
|
|
@@ -50,7 +50,7 @@ module ActiveJob
|
|
|
50
50
|
info do
|
|
51
51
|
jobs = event.payload[:jobs]
|
|
52
52
|
adapter = event.payload[:adapter]
|
|
53
|
-
enqueued_count = event.payload[:enqueued_count]
|
|
53
|
+
enqueued_count = event.payload[:enqueued_count].to_i
|
|
54
54
|
|
|
55
55
|
if enqueued_count == jobs.size
|
|
56
56
|
enqueued_jobs_message(adapter, jobs)
|
|
@@ -87,8 +87,9 @@ module ActiveJob
|
|
|
87
87
|
job = event.payload[:job]
|
|
88
88
|
ex = event.payload[:exception_object]
|
|
89
89
|
if ex
|
|
90
|
+
cleaned_backtrace = backtrace_cleaner.clean(ex.backtrace)
|
|
90
91
|
error do
|
|
91
|
-
"Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(
|
|
92
|
+
"Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(cleaned_backtrace).join("\n")
|
|
92
93
|
end
|
|
93
94
|
elsif event.payload[:aborted]
|
|
94
95
|
error do
|
|
@@ -125,7 +126,7 @@ module ActiveJob
|
|
|
125
126
|
"Stopped retrying #{job.class} (Job ID: #{job.job_id}) due to a #{ex.class} (#{ex.message}), which reoccurred on #{job.executions} attempts."
|
|
126
127
|
end
|
|
127
128
|
end
|
|
128
|
-
subscribe_log_level :
|
|
129
|
+
subscribe_log_level :retry_stopped, :error
|
|
129
130
|
|
|
130
131
|
def discard(event)
|
|
131
132
|
job = event.payload[:job]
|
|
@@ -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})"
|
|
@@ -189,15 +248,15 @@ module ActiveJob
|
|
|
189
248
|
end
|
|
190
249
|
|
|
191
250
|
def log_enqueue_source
|
|
192
|
-
source =
|
|
251
|
+
source = enqueue_source_location
|
|
193
252
|
|
|
194
253
|
if source
|
|
195
254
|
logger.info("↳ #{source}")
|
|
196
255
|
end
|
|
197
256
|
end
|
|
198
257
|
|
|
199
|
-
def
|
|
200
|
-
backtrace_cleaner.
|
|
258
|
+
def enqueue_source_location
|
|
259
|
+
backtrace_cleaner.first_clean_frame
|
|
201
260
|
end
|
|
202
261
|
|
|
203
262
|
def enqueued_jobs_message(adapter, enqueued_jobs)
|
data/lib/active_job/logging.rb
CHANGED
|
@@ -4,17 +4,31 @@ require "active_support/tagged_logging"
|
|
|
4
4
|
require "active_support/logger"
|
|
5
5
|
|
|
6
6
|
module ActiveJob
|
|
7
|
-
module Logging
|
|
7
|
+
module Logging
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
9
|
|
|
10
10
|
included do
|
|
11
|
+
##
|
|
12
|
+
# Accepts a logger conforming to the interface of Log4r or the default
|
|
13
|
+
# Ruby +Logger+ class. You can retrieve this logger by calling +logger+ on
|
|
14
|
+
# either an Active Job job class or an Active Job job instance.
|
|
11
15
|
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Configures whether a job's arguments should be logged. This can be
|
|
19
|
+
# useful when a job's arguments may be sensitive and so should not be
|
|
20
|
+
# logged.
|
|
21
|
+
#
|
|
22
|
+
# The value defaults to +true+, but this can be configured with
|
|
23
|
+
# +config.active_job.log_arguments+. Additionally, individual jobs can
|
|
24
|
+
# also configure a value, which will apply to themselves and any
|
|
25
|
+
# subclasses.
|
|
12
26
|
class_attribute :log_arguments, instance_accessor: false, default: true
|
|
13
27
|
|
|
14
28
|
around_enqueue(prepend: true) { |_, block| tag_logger(&block) }
|
|
15
29
|
end
|
|
16
30
|
|
|
17
|
-
def perform_now
|
|
31
|
+
def perform_now # :nodoc:
|
|
18
32
|
tag_logger(self.class.name, self.job_id) { super }
|
|
19
33
|
end
|
|
20
34
|
|
|
@@ -15,7 +15,8 @@ module ActiveJob
|
|
|
15
15
|
# = Active Job Queue adapter
|
|
16
16
|
#
|
|
17
17
|
# The +ActiveJob::QueueAdapter+ module is used to load the
|
|
18
|
-
# correct adapter. The default queue adapter is
|
|
18
|
+
# correct adapter. The default queue adapter is +:async+,
|
|
19
|
+
# which loads the ActiveJob::QueueAdapters::AsyncAdapter.
|
|
19
20
|
module QueueAdapter # :nodoc:
|
|
20
21
|
extend ActiveSupport::Concern
|
|
21
22
|
|
|
@@ -24,21 +25,21 @@ module ActiveJob
|
|
|
24
25
|
class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false
|
|
25
26
|
|
|
26
27
|
delegate :queue_adapter, to: :class
|
|
27
|
-
|
|
28
|
-
self.queue_adapter = :async
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
# Includes the setter method for changing the active queue adapter.
|
|
32
31
|
module ClassMethods
|
|
33
32
|
# Returns the backend queue provider. The default queue adapter
|
|
34
|
-
# is
|
|
33
|
+
# is +:async+. See QueueAdapters for more information.
|
|
35
34
|
def queue_adapter
|
|
35
|
+
self.queue_adapter = :async if _queue_adapter.nil?
|
|
36
36
|
_queue_adapter
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Returns string denoting the name of the configured queue adapter.
|
|
40
40
|
# By default returns <tt>"async"</tt>.
|
|
41
41
|
def queue_adapter_name
|
|
42
|
+
self.queue_adapter = :async if _queue_adapter_name.nil?
|
|
42
43
|
_queue_adapter_name
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -49,6 +50,7 @@ module ActiveJob
|
|
|
49
50
|
case name_or_adapter
|
|
50
51
|
when Symbol, String
|
|
51
52
|
queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new
|
|
53
|
+
queue_adapter.try(:check_adapter)
|
|
52
54
|
assign_adapter(name_or_adapter.to_s, queue_adapter)
|
|
53
55
|
else
|
|
54
56
|
if queue_adapter?(name_or_adapter)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module QueueAdapters
|
|
5
|
+
# = Active Job Abstract Adapter
|
|
6
|
+
#
|
|
7
|
+
# Active Job supports multiple job queue systems. ActiveJob::QueueAdapters::AbstractAdapter
|
|
8
|
+
# forms the abstraction layer which makes this possible.
|
|
9
|
+
class AbstractAdapter
|
|
10
|
+
attr_accessor :stopping
|
|
11
|
+
|
|
12
|
+
def enqueue(job)
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enqueue_at(job, timestamp)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stopping?
|
|
21
|
+
!!@stopping
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -30,7 +30,7 @@ module ActiveJob
|
|
|
30
30
|
# The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
|
|
31
31
|
# jobs. Since jobs share a single thread pool, long-running jobs will block
|
|
32
32
|
# short-lived jobs. Fine for dev/test; bad for production.
|
|
33
|
-
class AsyncAdapter
|
|
33
|
+
class AsyncAdapter < AbstractAdapter
|
|
34
34
|
# See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
|
|
35
35
|
def initialize(**executor_options)
|
|
36
36
|
@scheduler = Scheduler.new(**executor_options)
|
|
@@ -74,7 +74,7 @@ module ActiveJob
|
|
|
74
74
|
class Scheduler # :nodoc:
|
|
75
75
|
DEFAULT_EXECUTOR_OPTIONS = {
|
|
76
76
|
min_threads: 0,
|
|
77
|
-
max_threads:
|
|
77
|
+
max_threads: ENV.fetch("RAILS_MAX_THREADS", 5).to_i,
|
|
78
78
|
auto_terminate: true,
|
|
79
79
|
idletime: 60, # 1 minute
|
|
80
80
|
max_queue: 0, # unlimited
|
|
@@ -86,7 +86,11 @@ module ActiveJob
|
|
|
86
86
|
def initialize(**options)
|
|
87
87
|
self.immediate = false
|
|
88
88
|
@immediate_executor = Concurrent::ImmediateExecutor.new
|
|
89
|
-
@async_executor = Concurrent::ThreadPoolExecutor.new(
|
|
89
|
+
@async_executor = Concurrent::ThreadPoolExecutor.new(
|
|
90
|
+
name: "ActiveJob-async-scheduler",
|
|
91
|
+
**DEFAULT_EXECUTOR_OPTIONS,
|
|
92
|
+
**options
|
|
93
|
+
)
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
def enqueue(job, queue_name:)
|
|
@@ -14,7 +14,7 @@ module ActiveJob
|
|
|
14
14
|
# To use Backburner set the queue_adapter config to +:backburner+.
|
|
15
15
|
#
|
|
16
16
|
# Rails.application.config.active_job.queue_adapter = :backburner
|
|
17
|
-
class BackburnerAdapter
|
|
17
|
+
class BackburnerAdapter < AbstractAdapter
|
|
18
18
|
def enqueue(job) # :nodoc:
|
|
19
19
|
response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
|
|
20
20
|
job.provider_job_id = response[:id] if response.is_a?(Hash)
|