activejob 6.0.3.3 → 6.1.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.
@@ -54,9 +54,9 @@ module ActiveJob
54
54
 
55
55
  run_callbacks :enqueue do
56
56
  if scheduled_at
57
- self.class.queue_adapter.enqueue_at self, scheduled_at
57
+ queue_adapter.enqueue_at self, scheduled_at
58
58
  else
59
- self.class.queue_adapter.enqueue self
59
+ queue_adapter.enqueue self
60
60
  end
61
61
 
62
62
  successfully_enqueued = true
@@ -65,17 +65,7 @@ module ActiveJob
65
65
  if successfully_enqueued
66
66
  self
67
67
  else
68
- if self.class.return_false_on_aborted_enqueue
69
- false
70
- else
71
- ActiveSupport::Deprecation.warn(
72
- "Rails 6.1 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
73
- " returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \
74
- " to remove the deprecations."
75
- )
76
-
77
- self
78
- end
68
+ false
79
69
  end
80
70
  end
81
71
  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,23 +22,25 @@ 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 }
33
38
  #
34
39
  # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
35
40
  # retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
36
41
  # # To retry at most 10 times for each individual exception:
37
42
  # # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
43
+ # # retry_on Net::ReadTimeout, wait: 5.seconds, jitter: 0.30, attempts: 10
38
44
  # # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10
39
45
  #
40
46
  # retry_on(YetAnotherCustomAppException) do |job, error|
@@ -47,12 +53,11 @@ module ActiveJob
47
53
  # # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
48
54
  # end
49
55
  # end
50
- def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
56
+ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT)
51
57
  rescue_from(*exceptions) do |error|
52
58
  executions = executions_for(exceptions)
53
-
54
59
  if executions < attempts
55
- retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions), queue: queue, priority: priority, error: error
60
+ retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error
56
61
  else
57
62
  if block_given?
58
63
  instrument :retry_stopped, error: error do
@@ -115,22 +120,27 @@ module ActiveJob
115
120
  # end
116
121
  # end
117
122
  def retry_job(options = {})
118
- instrument :enqueue_retry, **options.slice(:error, :wait) do
123
+ instrument :enqueue_retry, options.slice(:error, :wait) do
119
124
  enqueue options
120
125
  end
121
126
  end
122
127
 
123
128
  private
124
- def determine_delay(seconds_or_duration_or_algorithm:, executions:)
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
+
125
135
  case seconds_or_duration_or_algorithm
126
136
  when :exponentially_longer
127
- (executions**4) + 2
128
- when ActiveSupport::Duration
129
- duration = seconds_or_duration_or_algorithm
130
- duration.to_i
131
- when Integer
132
- seconds = seconds_or_duration_or_algorithm
133
- 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
134
144
  when Proc
135
145
  algorithm = seconds_or_duration_or_algorithm
136
146
  algorithm.call(executions)
@@ -139,10 +149,9 @@ module ActiveJob
139
149
  end
140
150
  end
141
151
 
142
- def instrument(name, error: nil, wait: nil, &block)
143
- payload = { job: self, adapter: self.class.queue_adapter, error: error, wait: wait }
144
-
145
- ActiveSupport::Notifications.instrument("#{name}.active_job", payload, &block)
152
+ def determine_jitter_for_delay(delay, jitter)
153
+ return 0.0 if jitter.zero?
154
+ Kernel.rand * delay * jitter
146
155
  end
147
156
 
148
157
  def executions_for(exceptions)
@@ -29,13 +29,21 @@ module ActiveJob
29
29
 
30
30
  # Performs the job immediately. The job is not sent to the queuing adapter
31
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.
32
33
  #
33
- # 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!"
34
41
  def perform_now
35
42
  # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
36
43
  self.executions = (executions || 0) + 1
37
44
 
38
45
  deserialize_arguments_if_needed
46
+
39
47
  run_callbacks :perform do
40
48
  perform(*arguments)
41
49
  end
@@ -8,9 +8,9 @@ module ActiveJob
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 6
11
- MINOR = 0
12
- TINY = 3
13
- PRE = "3"
11
+ MINOR = 1
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/filters"
4
3
  require "active_support/tagged_logging"
5
4
  require "active_support/logger"
6
5
 
@@ -10,32 +9,10 @@ module ActiveJob
10
9
 
11
10
  included do
12
11
  cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
12
+ class_attribute :log_arguments, instance_accessor: false, default: true
13
13
 
14
- around_enqueue do |_, block|
15
- tag_logger do
16
- block.call
17
- end
18
- end
19
-
20
- around_perform do |job, block|
21
- tag_logger(job.class.name, job.job_id) do
22
- payload = { adapter: job.class.queue_adapter, job: job }
23
- ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
24
- ActiveSupport::Notifications.instrument("perform.active_job", payload) do
25
- block.call
26
- end
27
- end
28
- end
29
-
30
- around_enqueue do |job, block|
31
- if job.scheduled_at
32
- ActiveSupport::Notifications.instrument("enqueue_at.active_job",
33
- adapter: job.class.queue_adapter, job: job, &block)
34
- else
35
- ActiveSupport::Notifications.instrument("enqueue.active_job",
36
- adapter: job.class.queue_adapter, job: job, &block)
37
- end
38
- end
14
+ around_enqueue { |_, block| tag_logger(&block) }
15
+ around_perform { |job, block| tag_logger(job.class.name, job.job_id, &block) }
39
16
  end
40
17
 
41
18
  private
@@ -51,111 +28,5 @@ module ActiveJob
51
28
  def logger_tagged_by_active_job?
52
29
  logger.formatter.current_tags.include?("ActiveJob")
53
30
  end
54
-
55
- class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
56
- def enqueue(event)
57
- info do
58
- job = event.payload[:job]
59
- "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
60
- end
61
- end
62
-
63
- def enqueue_at(event)
64
- info do
65
- job = event.payload[:job]
66
- "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
67
- end
68
- end
69
-
70
- def perform_start(event)
71
- info do
72
- job = event.payload[:job]
73
- "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} enqueued at #{job.enqueued_at}" + args_info(job)
74
- end
75
- end
76
-
77
- def perform(event)
78
- job = event.payload[:job]
79
- ex = event.payload[:exception_object]
80
- if ex
81
- error do
82
- "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")
83
- end
84
- else
85
- info do
86
- "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms"
87
- end
88
- end
89
- end
90
-
91
- def enqueue_retry(event)
92
- job = event.payload[:job]
93
- ex = event.payload[:error]
94
- wait = event.payload[:wait]
95
-
96
- info do
97
- if ex
98
- "Retrying #{job.class} in #{wait.to_i} seconds, due to a #{ex.class}."
99
- else
100
- "Retrying #{job.class} in #{wait.to_i} seconds."
101
- end
102
- end
103
- end
104
-
105
- def retry_stopped(event)
106
- job = event.payload[:job]
107
- ex = event.payload[:error]
108
-
109
- error do
110
- "Stopped retrying #{job.class} due to a #{ex.class}, which reoccurred on #{job.executions} attempts."
111
- end
112
- end
113
-
114
- def discard(event)
115
- job = event.payload[:job]
116
- ex = event.payload[:error]
117
-
118
- error do
119
- "Discarded #{job.class} due to a #{ex.class}."
120
- end
121
- end
122
-
123
- private
124
- def queue_name(event)
125
- event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
126
- end
127
-
128
- def args_info(job)
129
- if job.arguments.any?
130
- " with arguments: " +
131
- job.arguments.map { |arg| format(arg).inspect }.join(", ")
132
- else
133
- ""
134
- end
135
- end
136
-
137
- def format(arg)
138
- case arg
139
- when Hash
140
- arg.transform_values { |value| format(value) }
141
- when Array
142
- arg.map { |value| format(value) }
143
- when GlobalID::Identification
144
- arg.to_global_id rescue arg
145
- else
146
- arg
147
- end
148
- end
149
-
150
- def scheduled_at(event)
151
- Time.at(event.payload[:job].scheduled_at).utc
152
- end
153
-
154
- def logger
155
- ActiveJob::Base.logger
156
- end
157
- end
158
31
  end
159
32
  end
160
-
161
- ActiveJob::Logging::LogSubscriber.attach_to :active_job