activejob 5.2.8.1 → 6.1.6.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 +118 -57
- data/MIT-LICENSE +1 -2
- data/README.md +18 -13
- data/lib/active_job/arguments.rb +80 -30
- data/lib/active_job/base.rb +6 -1
- data/lib/active_job/callbacks.rb +46 -3
- data/lib/active_job/configured_job.rb +2 -0
- data/lib/active_job/core.rb +40 -21
- data/lib/active_job/enqueuing.rb +20 -7
- data/lib/active_job/exceptions.rb +60 -28
- data/lib/active_job/execution.rb +11 -2
- data/lib/active_job/gem_version.rb +3 -3
- data/lib/active_job/instrumentation.rb +40 -0
- data/lib/active_job/log_subscriber.rb +140 -0
- data/lib/active_job/logging.rb +3 -101
- data/lib/active_job/queue_adapter.rb +5 -0
- data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
- data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/que_adapter.rb +2 -2
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +2 -2
- data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/test_adapter.rb +32 -14
- data/lib/active_job/queue_adapters.rb +13 -11
- data/lib/active_job/queue_name.rb +23 -3
- data/lib/active_job/railtie.rb +20 -1
- data/lib/active_job/serializers/date_serializer.rb +20 -0
- data/lib/active_job/serializers/date_time_serializer.rb +16 -0
- data/lib/active_job/serializers/duration_serializer.rb +23 -0
- data/lib/active_job/serializers/module_serializer.rb +20 -0
- data/lib/active_job/serializers/object_serializer.rb +53 -0
- data/lib/active_job/serializers/symbol_serializer.rb +20 -0
- data/lib/active_job/serializers/time_object_serializer.rb +13 -0
- data/lib/active_job/serializers/time_serializer.rb +16 -0
- data/lib/active_job/serializers/time_with_zone_serializer.rb +16 -0
- data/lib/active_job/serializers.rb +66 -0
- data/lib/active_job/test_helper.rb +317 -68
- data/lib/active_job/timezones.rb +13 -0
- data/lib/active_job/translation.rb +1 -1
- data/lib/active_job.rb +2 -1
- data/lib/rails/generators/job/job_generator.rb +4 -0
- metadata +25 -9
- data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
data/lib/active_job/callbacks.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support/callbacks"
|
|
4
|
+
require "active_support/core_ext/object/with_options"
|
|
5
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
4
6
|
|
|
5
7
|
module ActiveJob
|
|
6
8
|
# = Active Job Callbacks
|
|
@@ -27,13 +29,25 @@ module ActiveJob
|
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
included do
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
class_attribute :return_false_on_aborted_enqueue, instance_accessor: false, instance_predicate: false, default: false
|
|
33
|
+
singleton_class.deprecate :return_false_on_aborted_enqueue, :return_false_on_aborted_enqueue=
|
|
34
|
+
cattr_accessor :skip_after_callbacks_if_terminated, instance_accessor: false, default: false
|
|
35
|
+
|
|
36
|
+
with_options(skip_after_callbacks_if_terminated: skip_after_callbacks_if_terminated) do
|
|
37
|
+
define_callbacks :perform
|
|
38
|
+
define_callbacks :enqueue
|
|
39
|
+
end
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
# These methods will be included into any Active Job object, adding
|
|
35
43
|
# callbacks for +perform+ and +enqueue+ methods.
|
|
36
44
|
module ClassMethods
|
|
45
|
+
def inherited(klass)
|
|
46
|
+
klass.get_callbacks(:enqueue).config[:skip_after_callbacks_if_terminated] = skip_after_callbacks_if_terminated
|
|
47
|
+
klass.get_callbacks(:perform).config[:skip_after_callbacks_if_terminated] = skip_after_callbacks_if_terminated
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
# Defines a callback that will get called right before the
|
|
38
52
|
# job's perform method is executed.
|
|
39
53
|
#
|
|
@@ -88,6 +102,19 @@ module ActiveJob
|
|
|
88
102
|
# end
|
|
89
103
|
# end
|
|
90
104
|
#
|
|
105
|
+
# You can access the return value of the job only if the execution wasn't halted.
|
|
106
|
+
#
|
|
107
|
+
# class VideoProcessJob < ActiveJob::Base
|
|
108
|
+
# around_perform do |job, block|
|
|
109
|
+
# value = block.call
|
|
110
|
+
# puts value # => "Hello World!"
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# def perform
|
|
114
|
+
# "Hello World!"
|
|
115
|
+
# end
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
91
118
|
def around_perform(*filters, &blk)
|
|
92
119
|
set_callback(:perform, :around, *filters, &blk)
|
|
93
120
|
end
|
|
@@ -130,7 +157,7 @@ module ActiveJob
|
|
|
130
157
|
set_callback(:enqueue, :after, *filters, &blk)
|
|
131
158
|
end
|
|
132
159
|
|
|
133
|
-
# Defines a callback that will get called around the
|
|
160
|
+
# Defines a callback that will get called around the enqueuing
|
|
134
161
|
# of the job.
|
|
135
162
|
#
|
|
136
163
|
# class VideoProcessJob < ActiveJob::Base
|
|
@@ -151,5 +178,21 @@ module ActiveJob
|
|
|
151
178
|
set_callback(:enqueue, :around, *filters, &blk)
|
|
152
179
|
end
|
|
153
180
|
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
def halted_callback_hook(_filter, name) # :nodoc:
|
|
184
|
+
return super unless %i(enqueue perform).include?(name.to_sym)
|
|
185
|
+
callbacks = public_send("_#{name}_callbacks")
|
|
186
|
+
|
|
187
|
+
if !self.class.skip_after_callbacks_if_terminated && callbacks.any? { |c| c.kind == :after }
|
|
188
|
+
ActiveSupport::Deprecation.warn(<<~EOM)
|
|
189
|
+
In Rails 7.0, `after_enqueue`/`after_perform` callbacks no longer run if `before_enqueue`/`before_perform` respectively halts with `throw :abort`.
|
|
190
|
+
To enable this behavior, uncomment the `config.active_job.skip_after_callbacks_if_terminated` config
|
|
191
|
+
in the new 6.1 framework defaults initializer.
|
|
192
|
+
EOM
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
super
|
|
196
|
+
end
|
|
154
197
|
end
|
|
155
198
|
end
|
|
@@ -10,9 +10,11 @@ module ActiveJob
|
|
|
10
10
|
def perform_now(*args)
|
|
11
11
|
@job_class.new(*args).perform_now
|
|
12
12
|
end
|
|
13
|
+
ruby2_keywords(:perform_now) if respond_to?(:ruby2_keywords, true)
|
|
13
14
|
|
|
14
15
|
def perform_later(*args)
|
|
15
16
|
@job_class.new(*args).enqueue @options
|
|
16
17
|
end
|
|
18
|
+
ruby2_keywords(:perform_later) if respond_to?(:ruby2_keywords, true)
|
|
17
19
|
end
|
|
18
20
|
end
|
data/lib/active_job/core.rb
CHANGED
|
@@ -6,32 +6,42 @@ module ActiveJob
|
|
|
6
6
|
module Core
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
attr_writer :serialized_arguments
|
|
9
|
+
# Job arguments
|
|
10
|
+
attr_accessor :arguments
|
|
11
|
+
attr_writer :serialized_arguments
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# Timestamp when the job should be performed
|
|
14
|
+
attr_accessor :scheduled_at
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# Job Identifier
|
|
17
|
+
attr_accessor :job_id
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
# Queue in which the job will reside.
|
|
20
|
+
attr_writer :queue_name
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# Priority that the job will have (lower is more priority).
|
|
23
|
+
attr_writer :priority
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
# ID optionally provided by adapter
|
|
26
|
+
attr_accessor :provider_job_id
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# Number of times this job has been executed (which increments on every retry, like after an exception).
|
|
29
|
+
attr_accessor :executions
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
# Hash that contains the number of times this job handled errors for each specific retry_on declaration.
|
|
32
|
+
# Keys are the string representation of the exceptions listed in the retry_on declaration,
|
|
33
|
+
# while its associated value holds the number of executions where the corresponding retry_on
|
|
34
|
+
# declaration handled one of its listed exceptions.
|
|
35
|
+
attr_accessor :exception_executions
|
|
36
|
+
|
|
37
|
+
# I18n.locale to be used during the job.
|
|
38
|
+
attr_accessor :locale
|
|
39
|
+
|
|
40
|
+
# Timezone to be used during the job.
|
|
41
|
+
attr_accessor :timezone
|
|
42
|
+
|
|
43
|
+
# Track when a job was enqueued
|
|
44
|
+
attr_accessor :enqueued_at
|
|
35
45
|
|
|
36
46
|
# These methods will be included into any Active Job object, adding
|
|
37
47
|
# helpers for de/serialization and creation of job instances.
|
|
@@ -74,10 +84,13 @@ module ActiveJob
|
|
|
74
84
|
@queue_name = self.class.queue_name
|
|
75
85
|
@priority = self.class.priority
|
|
76
86
|
@executions = 0
|
|
87
|
+
@exception_executions = {}
|
|
88
|
+
@timezone = Time.zone&.name
|
|
77
89
|
end
|
|
90
|
+
ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
|
|
78
91
|
|
|
79
92
|
# Returns a hash with the job data that can safely be passed to the
|
|
80
|
-
#
|
|
93
|
+
# queuing adapter.
|
|
81
94
|
def serialize
|
|
82
95
|
{
|
|
83
96
|
"job_class" => self.class.name,
|
|
@@ -87,7 +100,10 @@ module ActiveJob
|
|
|
87
100
|
"priority" => priority,
|
|
88
101
|
"arguments" => serialize_arguments_if_needed(arguments),
|
|
89
102
|
"executions" => executions,
|
|
90
|
-
"
|
|
103
|
+
"exception_executions" => exception_executions,
|
|
104
|
+
"locale" => I18n.locale.to_s,
|
|
105
|
+
"timezone" => timezone,
|
|
106
|
+
"enqueued_at" => Time.now.utc.iso8601
|
|
91
107
|
}
|
|
92
108
|
end
|
|
93
109
|
|
|
@@ -124,7 +140,10 @@ module ActiveJob
|
|
|
124
140
|
self.priority = job_data["priority"]
|
|
125
141
|
self.serialized_arguments = job_data["arguments"]
|
|
126
142
|
self.executions = job_data["executions"]
|
|
143
|
+
self.exception_executions = job_data["exception_executions"]
|
|
127
144
|
self.locale = job_data["locale"] || I18n.locale.to_s
|
|
145
|
+
self.timezone = job_data["timezone"] || Time.zone&.name
|
|
146
|
+
self.enqueued_at = job_data["enqueued_at"]
|
|
128
147
|
end
|
|
129
148
|
|
|
130
149
|
private
|
data/lib/active_job/enqueuing.rb
CHANGED
|
@@ -9,21 +9,25 @@ module ActiveJob
|
|
|
9
9
|
|
|
10
10
|
# Includes the +perform_later+ method for job initialization.
|
|
11
11
|
module ClassMethods
|
|
12
|
-
# Push a job onto the queue.
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
12
|
+
# Push a job onto the queue. By default the arguments must be either String,
|
|
13
|
+
# Integer, Float, NilClass, TrueClass, FalseClass, BigDecimal, Symbol, Date,
|
|
14
|
+
# Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::Duration,
|
|
15
|
+
# Hash, ActiveSupport::HashWithIndifferentAccess, Array or
|
|
16
|
+
# GlobalID::Identification instances, although this can be extended by adding
|
|
17
|
+
# custom serializers.
|
|
16
18
|
#
|
|
17
19
|
# Returns an instance of the job class queued with arguments available in
|
|
18
20
|
# Job#arguments.
|
|
19
21
|
def perform_later(*args)
|
|
20
22
|
job_or_instantiate(*args).enqueue
|
|
21
23
|
end
|
|
24
|
+
ruby2_keywords(:perform_later) if respond_to?(:ruby2_keywords, true)
|
|
22
25
|
|
|
23
26
|
private
|
|
24
27
|
def job_or_instantiate(*args) # :doc:
|
|
25
28
|
args.first.is_a?(self) ? args.first : new(*args)
|
|
26
29
|
end
|
|
30
|
+
ruby2_keywords(:job_or_instantiate) if respond_to?(:ruby2_keywords, true)
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
# Enqueues the job to be performed by the queue adapter.
|
|
@@ -46,14 +50,23 @@ module ActiveJob
|
|
|
46
50
|
self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
|
|
47
51
|
self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
|
|
48
52
|
self.priority = options[:priority].to_i if options[:priority]
|
|
53
|
+
successfully_enqueued = false
|
|
54
|
+
|
|
49
55
|
run_callbacks :enqueue do
|
|
50
56
|
if scheduled_at
|
|
51
|
-
|
|
57
|
+
queue_adapter.enqueue_at self, scheduled_at
|
|
52
58
|
else
|
|
53
|
-
|
|
59
|
+
queue_adapter.enqueue self
|
|
54
60
|
end
|
|
61
|
+
|
|
62
|
+
successfully_enqueued = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if successfully_enqueued
|
|
66
|
+
self
|
|
67
|
+
else
|
|
68
|
+
false
|
|
55
69
|
end
|
|
56
|
-
self
|
|
57
70
|
end
|
|
58
71
|
end
|
|
59
72
|
end
|
|
@@ -7,6 +7,10 @@ module ActiveJob
|
|
|
7
7
|
module Exceptions
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
9
|
|
|
10
|
+
included do
|
|
11
|
+
class_attribute :retry_jitter, instance_accessor: false, instance_predicate: false, default: 0.0
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
module ClassMethods
|
|
11
15
|
# Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts.
|
|
12
16
|
# If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to
|
|
@@ -18,40 +22,49 @@ module ActiveJob
|
|
|
18
22
|
#
|
|
19
23
|
# ==== Options
|
|
20
24
|
# * <tt>:wait</tt> - Re-enqueues the job with a delay specified either in seconds (default: 3 seconds),
|
|
21
|
-
# as a computing proc that the number of executions so far as an argument, or as a symbol reference of
|
|
22
|
-
# <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions **
|
|
23
|
-
# (first wait 3s, then 18s, then 83s, etc)
|
|
25
|
+
# as a computing proc that takes the number of executions so far as an argument, or as a symbol reference of
|
|
26
|
+
# <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>((executions**4) + (Kernel.rand * (executions**4) * jitter)) + 2</tt>
|
|
27
|
+
# (first wait ~3s, then ~18s, then ~83s, etc)
|
|
24
28
|
# * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts)
|
|
25
29
|
# * <tt>:queue</tt> - Re-enqueues the job on a different queue
|
|
26
30
|
# * <tt>:priority</tt> - Re-enqueues the job with a different priority
|
|
31
|
+
# * <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)
|
|
27
32
|
#
|
|
28
33
|
# ==== Examples
|
|
29
34
|
#
|
|
30
35
|
# class RemoteServiceJob < ActiveJob::Base
|
|
31
|
-
# retry_on CustomAppException # defaults to 3s wait, 5 attempts
|
|
36
|
+
# retry_on CustomAppException # defaults to ~3s wait, 5 attempts
|
|
32
37
|
# retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
|
|
38
|
+
#
|
|
39
|
+
# retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
|
|
40
|
+
# retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
|
|
41
|
+
# # To retry at most 10 times for each individual exception:
|
|
42
|
+
# # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
|
|
43
|
+
# # retry_on Net::ReadTimeout, wait: 5.seconds, jitter: 0.30, attempts: 10
|
|
44
|
+
# # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10
|
|
45
|
+
#
|
|
33
46
|
# retry_on(YetAnotherCustomAppException) do |job, error|
|
|
34
47
|
# ExceptionNotifier.caught(error)
|
|
35
48
|
# end
|
|
36
|
-
# retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
|
|
37
|
-
# retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
|
|
38
49
|
#
|
|
39
50
|
# def perform(*args)
|
|
40
51
|
# # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
|
|
41
52
|
# # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
|
|
42
|
-
# # Might raise Net::OpenTimeout when the remote service is down
|
|
53
|
+
# # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
|
|
43
54
|
# end
|
|
44
55
|
# end
|
|
45
|
-
def retry_on(
|
|
46
|
-
rescue_from
|
|
56
|
+
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT)
|
|
57
|
+
rescue_from(*exceptions) do |error|
|
|
58
|
+
executions = executions_for(exceptions)
|
|
47
59
|
if executions < attempts
|
|
48
|
-
|
|
49
|
-
retry_job wait: determine_delay(wait), queue: queue, priority: priority
|
|
60
|
+
retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error
|
|
50
61
|
else
|
|
51
62
|
if block_given?
|
|
52
|
-
|
|
63
|
+
instrument :retry_stopped, error: error do
|
|
64
|
+
yield self, error
|
|
65
|
+
end
|
|
53
66
|
else
|
|
54
|
-
|
|
67
|
+
instrument :retry_stopped, error: error
|
|
55
68
|
raise error
|
|
56
69
|
end
|
|
57
70
|
end
|
|
@@ -76,12 +89,10 @@ module ActiveJob
|
|
|
76
89
|
# # Might raise CustomAppException for something domain specific
|
|
77
90
|
# end
|
|
78
91
|
# end
|
|
79
|
-
def discard_on(
|
|
80
|
-
rescue_from
|
|
81
|
-
|
|
82
|
-
yield self, error
|
|
83
|
-
else
|
|
84
|
-
logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
|
|
92
|
+
def discard_on(*exceptions)
|
|
93
|
+
rescue_from(*exceptions) do |error|
|
|
94
|
+
instrument :discard, error: error do
|
|
95
|
+
yield self, error if block_given?
|
|
85
96
|
end
|
|
86
97
|
end
|
|
87
98
|
end
|
|
@@ -109,20 +120,27 @@ module ActiveJob
|
|
|
109
120
|
# end
|
|
110
121
|
# end
|
|
111
122
|
def retry_job(options = {})
|
|
112
|
-
|
|
123
|
+
instrument :enqueue_retry, options.slice(:error, :wait) do
|
|
124
|
+
enqueue options
|
|
125
|
+
end
|
|
113
126
|
end
|
|
114
127
|
|
|
115
128
|
private
|
|
116
|
-
|
|
129
|
+
JITTER_DEFAULT = Object.new
|
|
130
|
+
private_constant :JITTER_DEFAULT
|
|
131
|
+
|
|
132
|
+
def determine_delay(seconds_or_duration_or_algorithm:, executions:, jitter: JITTER_DEFAULT)
|
|
133
|
+
jitter = jitter == JITTER_DEFAULT ? self.class.retry_jitter : (jitter || 0.0)
|
|
134
|
+
|
|
117
135
|
case seconds_or_duration_or_algorithm
|
|
118
136
|
when :exponentially_longer
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
delay = executions**4
|
|
138
|
+
delay_jitter = determine_jitter_for_delay(delay, jitter)
|
|
139
|
+
delay + delay_jitter + 2
|
|
140
|
+
when ActiveSupport::Duration, Integer
|
|
141
|
+
delay = seconds_or_duration_or_algorithm.to_i
|
|
142
|
+
delay_jitter = determine_jitter_for_delay(delay, jitter)
|
|
143
|
+
delay + delay_jitter
|
|
126
144
|
when Proc
|
|
127
145
|
algorithm = seconds_or_duration_or_algorithm
|
|
128
146
|
algorithm.call(executions)
|
|
@@ -130,5 +148,19 @@ module ActiveJob
|
|
|
130
148
|
raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
|
|
131
149
|
end
|
|
132
150
|
end
|
|
151
|
+
|
|
152
|
+
def determine_jitter_for_delay(delay, jitter)
|
|
153
|
+
return 0.0 if jitter.zero?
|
|
154
|
+
Kernel.rand * delay * jitter
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def executions_for(exceptions)
|
|
158
|
+
if exception_executions
|
|
159
|
+
exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
|
|
160
|
+
else
|
|
161
|
+
# Guard against jobs that were persisted before we started having individual executions counters per retry_on
|
|
162
|
+
executions
|
|
163
|
+
end
|
|
164
|
+
end
|
|
133
165
|
end
|
|
134
166
|
end
|
data/lib/active_job/execution.rb
CHANGED
|
@@ -17,6 +17,7 @@ module ActiveJob
|
|
|
17
17
|
def perform_now(*args)
|
|
18
18
|
job_or_instantiate(*args).perform_now
|
|
19
19
|
end
|
|
20
|
+
ruby2_keywords(:perform_now) if respond_to?(:ruby2_keywords, true)
|
|
20
21
|
|
|
21
22
|
def execute(job_data) #:nodoc:
|
|
22
23
|
ActiveJob::Callbacks.run_callbacks(:execute) do
|
|
@@ -26,15 +27,23 @@ module ActiveJob
|
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
# Performs the job immediately. The job is not sent to the
|
|
30
|
+
# Performs the job immediately. The job is not sent to the queuing adapter
|
|
30
31
|
# but directly executed by blocking the execution of others until it's finished.
|
|
32
|
+
# +perform_now+ returns the value of your job's +perform+ method.
|
|
31
33
|
#
|
|
32
|
-
# MyJob
|
|
34
|
+
# class MyJob < ActiveJob::Base
|
|
35
|
+
# def perform
|
|
36
|
+
# "Hello World!"
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# puts MyJob.new(*args).perform_now # => "Hello World!"
|
|
33
41
|
def perform_now
|
|
34
42
|
# Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
|
|
35
43
|
self.executions = (executions || 0) + 1
|
|
36
44
|
|
|
37
45
|
deserialize_arguments_if_needed
|
|
46
|
+
|
|
38
47
|
run_callbacks :perform do
|
|
39
48
|
perform(*arguments)
|
|
40
49
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Instrumentation #:nodoc:
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
around_enqueue do |_, block|
|
|
9
|
+
scheduled_at ? instrument(:enqueue_at, &block) : instrument(:enqueue, &block)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
around_perform do |_, block|
|
|
13
|
+
instrument :perform_start
|
|
14
|
+
instrument :perform, &block
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
def instrument(operation, payload = {}, &block)
|
|
20
|
+
enhanced_block = ->(event_payload) do
|
|
21
|
+
value = block.call if block
|
|
22
|
+
|
|
23
|
+
if defined?(@_halted_callback_hook_called) && @_halted_callback_hook_called
|
|
24
|
+
event_payload[:aborted] = true
|
|
25
|
+
@_halted_callback_hook_called = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ActiveSupport::Notifications.instrument \
|
|
32
|
+
"#{operation}.active_job", payload.merge(adapter: queue_adapter, job: self), &enhanced_block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def halted_callback_hook(*)
|
|
36
|
+
super
|
|
37
|
+
@_halted_callback_hook_called = true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/filters"
|
|
4
|
+
require "active_support/log_subscriber"
|
|
5
|
+
|
|
6
|
+
module ActiveJob
|
|
7
|
+
class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
|
|
8
|
+
def enqueue(event)
|
|
9
|
+
job = event.payload[:job]
|
|
10
|
+
ex = event.payload[:exception_object]
|
|
11
|
+
|
|
12
|
+
if ex
|
|
13
|
+
error do
|
|
14
|
+
"Failed enqueuing #{job.class.name} to #{queue_name(event)}: #{ex.class} (#{ex.message})"
|
|
15
|
+
end
|
|
16
|
+
elsif event.payload[:aborted]
|
|
17
|
+
info do
|
|
18
|
+
"Failed enqueuing #{job.class.name} to #{queue_name(event)}, a before_enqueue callback halted the enqueuing execution."
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
info do
|
|
22
|
+
"Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def enqueue_at(event)
|
|
28
|
+
job = event.payload[:job]
|
|
29
|
+
ex = event.payload[:exception_object]
|
|
30
|
+
|
|
31
|
+
if ex
|
|
32
|
+
error do
|
|
33
|
+
"Failed enqueuing #{job.class.name} to #{queue_name(event)}: #{ex.class} (#{ex.message})"
|
|
34
|
+
end
|
|
35
|
+
elsif event.payload[:aborted]
|
|
36
|
+
info do
|
|
37
|
+
"Failed enqueuing #{job.class.name} to #{queue_name(event)}, a before_enqueue callback halted the enqueuing execution."
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
info do
|
|
41
|
+
"Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def perform_start(event)
|
|
47
|
+
info do
|
|
48
|
+
job = event.payload[:job]
|
|
49
|
+
"Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} enqueued at #{job.enqueued_at}" + args_info(job)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def perform(event)
|
|
54
|
+
job = event.payload[:job]
|
|
55
|
+
ex = event.payload[:exception_object]
|
|
56
|
+
if ex
|
|
57
|
+
error do
|
|
58
|
+
"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")
|
|
59
|
+
end
|
|
60
|
+
elsif event.payload[:aborted]
|
|
61
|
+
error do
|
|
62
|
+
"Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: a before_perform callback halted the job execution"
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
info do
|
|
66
|
+
"Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def enqueue_retry(event)
|
|
72
|
+
job = event.payload[:job]
|
|
73
|
+
ex = event.payload[:error]
|
|
74
|
+
wait = event.payload[:wait]
|
|
75
|
+
|
|
76
|
+
info do
|
|
77
|
+
if ex
|
|
78
|
+
"Retrying #{job.class} in #{wait.to_i} seconds, due to a #{ex.class}."
|
|
79
|
+
else
|
|
80
|
+
"Retrying #{job.class} in #{wait.to_i} seconds."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def retry_stopped(event)
|
|
86
|
+
job = event.payload[:job]
|
|
87
|
+
ex = event.payload[:error]
|
|
88
|
+
|
|
89
|
+
error do
|
|
90
|
+
"Stopped retrying #{job.class} due to a #{ex.class}, which reoccurred on #{job.executions} attempts."
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def discard(event)
|
|
95
|
+
job = event.payload[:job]
|
|
96
|
+
ex = event.payload[:error]
|
|
97
|
+
|
|
98
|
+
error do
|
|
99
|
+
"Discarded #{job.class} due to a #{ex.class}."
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
def queue_name(event)
|
|
105
|
+
event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def args_info(job)
|
|
109
|
+
if job.class.log_arguments? && job.arguments.any?
|
|
110
|
+
" with arguments: " +
|
|
111
|
+
job.arguments.map { |arg| format(arg).inspect }.join(", ")
|
|
112
|
+
else
|
|
113
|
+
""
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def format(arg)
|
|
118
|
+
case arg
|
|
119
|
+
when Hash
|
|
120
|
+
arg.transform_values { |value| format(value) }
|
|
121
|
+
when Array
|
|
122
|
+
arg.map { |value| format(value) }
|
|
123
|
+
when GlobalID::Identification
|
|
124
|
+
arg.to_global_id rescue arg
|
|
125
|
+
else
|
|
126
|
+
arg
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def scheduled_at(event)
|
|
131
|
+
Time.at(event.payload[:job].scheduled_at).utc
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def logger
|
|
135
|
+
ActiveJob::Base.logger
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
ActiveJob::LogSubscriber.attach_to :active_job
|