activejob 5.2.4.4 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -50
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +18 -13
  5. data/lib/active_job.rb +2 -1
  6. data/lib/active_job/arguments.rb +80 -30
  7. data/lib/active_job/base.rb +6 -1
  8. data/lib/active_job/callbacks.rb +46 -3
  9. data/lib/active_job/configured_job.rb +2 -0
  10. data/lib/active_job/core.rb +40 -21
  11. data/lib/active_job/enqueuing.rb +20 -7
  12. data/lib/active_job/exceptions.rb +60 -28
  13. data/lib/active_job/execution.rb +11 -2
  14. data/lib/active_job/gem_version.rb +4 -4
  15. data/lib/active_job/instrumentation.rb +40 -0
  16. data/lib/active_job/log_subscriber.rb +140 -0
  17. data/lib/active_job/logging.rb +3 -101
  18. data/lib/active_job/queue_adapter.rb +5 -0
  19. data/lib/active_job/queue_adapters.rb +13 -11
  20. data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
  21. data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
  22. data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
  23. data/lib/active_job/queue_adapters/que_adapter.rb +2 -2
  24. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +2 -2
  25. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +1 -1
  26. data/lib/active_job/queue_adapters/test_adapter.rb +32 -14
  27. data/lib/active_job/queue_name.rb +23 -3
  28. data/lib/active_job/railtie.rb +20 -1
  29. data/lib/active_job/serializers.rb +66 -0
  30. data/lib/active_job/serializers/date_serializer.rb +20 -0
  31. data/lib/active_job/serializers/date_time_serializer.rb +16 -0
  32. data/lib/active_job/serializers/duration_serializer.rb +23 -0
  33. data/lib/active_job/serializers/module_serializer.rb +20 -0
  34. data/lib/active_job/serializers/object_serializer.rb +53 -0
  35. data/lib/active_job/serializers/symbol_serializer.rb +20 -0
  36. data/lib/active_job/serializers/time_object_serializer.rb +13 -0
  37. data/lib/active_job/serializers/time_serializer.rb +16 -0
  38. data/lib/active_job/serializers/time_with_zone_serializer.rb +16 -0
  39. data/lib/active_job/test_helper.rb +316 -68
  40. data/lib/active_job/timezones.rb +13 -0
  41. data/lib/active_job/translation.rb +1 -1
  42. data/lib/rails/generators/job/job_generator.rb +4 -0
  43. metadata +29 -14
  44. data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
@@ -8,7 +8,10 @@ require "active_job/enqueuing"
8
8
  require "active_job/execution"
9
9
  require "active_job/callbacks"
10
10
  require "active_job/exceptions"
11
+ require "active_job/log_subscriber"
11
12
  require "active_job/logging"
13
+ require "active_job/instrumentation"
14
+ require "active_job/timezones"
12
15
  require "active_job/translation"
13
16
 
14
17
  module ActiveJob #:nodoc:
@@ -39,7 +42,7 @@ module ActiveJob #:nodoc:
39
42
  # Records that are passed in are serialized/deserialized using Global
40
43
  # ID. More information can be found in Arguments.
41
44
  #
42
- # To enqueue a job to be performed as soon as the queueing system is free:
45
+ # To enqueue a job to be performed as soon as the queuing system is free:
43
46
  #
44
47
  # ProcessPhotoJob.perform_later(photo)
45
48
  #
@@ -67,6 +70,8 @@ module ActiveJob #:nodoc:
67
70
  include Callbacks
68
71
  include Exceptions
69
72
  include Logging
73
+ include Instrumentation
74
+ include Timezones
70
75
  include Translation
71
76
 
72
77
  ActiveSupport.run_load_hooks(:active_job, self)
@@ -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
- define_callbacks :perform
31
- define_callbacks :enqueue
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 enqueueing
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 6.2, `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
@@ -6,32 +6,42 @@ module ActiveJob
6
6
  module Core
7
7
  extend ActiveSupport::Concern
8
8
 
9
- included do
10
- # Job arguments
11
- attr_accessor :arguments
12
- attr_writer :serialized_arguments
9
+ # Job arguments
10
+ attr_accessor :arguments
11
+ attr_writer :serialized_arguments
13
12
 
14
- # Timestamp when the job should be performed
15
- attr_accessor :scheduled_at
13
+ # Timestamp when the job should be performed
14
+ attr_accessor :scheduled_at
16
15
 
17
- # Job Identifier
18
- attr_accessor :job_id
16
+ # Job Identifier
17
+ attr_accessor :job_id
19
18
 
20
- # Queue in which the job will reside.
21
- attr_writer :queue_name
19
+ # Queue in which the job will reside.
20
+ attr_writer :queue_name
22
21
 
23
- # Priority that the job will have (lower is more priority).
24
- attr_writer :priority
22
+ # Priority that the job will have (lower is more priority).
23
+ attr_writer :priority
25
24
 
26
- # ID optionally provided by adapter
27
- attr_accessor :provider_job_id
25
+ # ID optionally provided by adapter
26
+ attr_accessor :provider_job_id
28
27
 
29
- # Number of times this job has been executed (which increments on every retry, like after an exception).
30
- attr_accessor :executions
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
- # I18n.locale to be used during the job.
33
- attr_accessor :locale
34
- end
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
- # queueing adapter.
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
- "locale" => I18n.locale.to_s
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
@@ -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. The arguments must be legal JSON types
13
- # (+string+, +int+, +float+, +nil+, +true+, +false+, +hash+ or +array+) or
14
- # GlobalID::Identification instances. Arbitrary Ruby objects
15
- # are not supported.
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
- self.class.queue_adapter.enqueue_at self, scheduled_at
57
+ queue_adapter.enqueue_at self, scheduled_at
52
58
  else
53
- self.class.queue_adapter.enqueue self
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 ** 4) + 2</tt>
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(exception, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
46
- rescue_from exception do |error|
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
- logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{exception}. The original exception was #{error.cause.inspect}."
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
- yield self, error
63
+ instrument :retry_stopped, error: error do
64
+ yield self, error
65
+ end
53
66
  else
54
- logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
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(exception)
80
- rescue_from exception do |error|
81
- if block_given?
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
- enqueue options
123
+ instrument :enqueue_retry, options.slice(:error, :wait) do
124
+ enqueue options
125
+ end
113
126
  end
114
127
 
115
128
  private
116
- def determine_delay(seconds_or_duration_or_algorithm)
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
- (executions**4) + 2
120
- when ActiveSupport::Duration
121
- duration = seconds_or_duration_or_algorithm
122
- duration.to_i
123
- when Integer
124
- seconds = seconds_or_duration_or_algorithm
125
- seconds
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
@@ -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 queueing adapter
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.new(*args).perform_now
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