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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +118 -57
  3. data/MIT-LICENSE +1 -2
  4. data/README.md +18 -13
  5. data/lib/active_job/arguments.rb +80 -30
  6. data/lib/active_job/base.rb +6 -1
  7. data/lib/active_job/callbacks.rb +46 -3
  8. data/lib/active_job/configured_job.rb +2 -0
  9. data/lib/active_job/core.rb +40 -21
  10. data/lib/active_job/enqueuing.rb +20 -7
  11. data/lib/active_job/exceptions.rb +60 -28
  12. data/lib/active_job/execution.rb +11 -2
  13. data/lib/active_job/gem_version.rb +3 -3
  14. data/lib/active_job/instrumentation.rb +40 -0
  15. data/lib/active_job/log_subscriber.rb +140 -0
  16. data/lib/active_job/logging.rb +3 -101
  17. data/lib/active_job/queue_adapter.rb +5 -0
  18. data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
  19. data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
  20. data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
  21. data/lib/active_job/queue_adapters/que_adapter.rb +2 -2
  22. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +2 -2
  23. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +1 -1
  24. data/lib/active_job/queue_adapters/test_adapter.rb +32 -14
  25. data/lib/active_job/queue_adapters.rb +13 -11
  26. data/lib/active_job/queue_name.rb +23 -3
  27. data/lib/active_job/railtie.rb +20 -1
  28. data/lib/active_job/serializers/date_serializer.rb +20 -0
  29. data/lib/active_job/serializers/date_time_serializer.rb +16 -0
  30. data/lib/active_job/serializers/duration_serializer.rb +23 -0
  31. data/lib/active_job/serializers/module_serializer.rb +20 -0
  32. data/lib/active_job/serializers/object_serializer.rb +53 -0
  33. data/lib/active_job/serializers/symbol_serializer.rb +20 -0
  34. data/lib/active_job/serializers/time_object_serializer.rb +13 -0
  35. data/lib/active_job/serializers/time_serializer.rb +16 -0
  36. data/lib/active_job/serializers/time_with_zone_serializer.rb +16 -0
  37. data/lib/active_job/serializers.rb +66 -0
  38. data/lib/active_job/test_helper.rb +317 -68
  39. data/lib/active_job/timezones.rb +13 -0
  40. data/lib/active_job/translation.rb +1 -1
  41. data/lib/active_job.rb +2 -1
  42. data/lib/rails/generators/job/job_generator.rb +4 -0
  43. metadata +25 -9
  44. data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
@@ -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 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
@@ -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
@@ -7,9 +7,9 @@ module ActiveJob
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 8
10
+ MAJOR = 6
11
+ MINOR = 1
12
+ TINY = 6
13
13
  PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -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