activejob 5.2.4.1 → 6.0.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -35
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +17 -10
  5. data/lib/active_job.rb +2 -1
  6. data/lib/active_job/arguments.rb +40 -28
  7. data/lib/active_job/base.rb +3 -1
  8. data/lib/active_job/callbacks.rb +4 -1
  9. data/lib/active_job/core.rb +38 -21
  10. data/lib/active_job/enqueuing.rb +26 -5
  11. data/lib/active_job/exceptions.rb +40 -17
  12. data/lib/active_job/execution.rb +1 -1
  13. data/lib/active_job/gem_version.rb +4 -4
  14. data/lib/active_job/logging.rb +40 -9
  15. data/lib/active_job/queue_adapter.rb +2 -0
  16. data/lib/active_job/queue_adapters.rb +8 -10
  17. data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
  18. data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
  19. data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
  20. data/lib/active_job/queue_adapters/test_adapter.rb +22 -8
  21. data/lib/active_job/queue_name.rb +21 -1
  22. data/lib/active_job/railtie.rb +16 -1
  23. data/lib/active_job/serializers.rb +63 -0
  24. data/lib/active_job/serializers/date_serializer.rb +21 -0
  25. data/lib/active_job/serializers/date_time_serializer.rb +21 -0
  26. data/lib/active_job/serializers/duration_serializer.rb +24 -0
  27. data/lib/active_job/serializers/object_serializer.rb +54 -0
  28. data/lib/active_job/serializers/symbol_serializer.rb +21 -0
  29. data/lib/active_job/serializers/time_serializer.rb +21 -0
  30. data/lib/active_job/serializers/time_with_zone_serializer.rb +21 -0
  31. data/lib/active_job/test_helper.rb +278 -57
  32. data/lib/active_job/timezones.rb +13 -0
  33. data/lib/active_job/translation.rb +1 -1
  34. data/lib/rails/generators/job/job_generator.rb +4 -0
  35. metadata +21 -10
  36. data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
@@ -29,6 +29,9 @@ module ActiveJob
29
29
  included do
30
30
  define_callbacks :perform
31
31
  define_callbacks :enqueue
32
+
33
+ class_attribute :return_false_on_aborted_enqueue, instance_accessor: false, instance_predicate: false
34
+ self.return_false_on_aborted_enqueue = false
32
35
  end
33
36
 
34
37
  # These methods will be included into any Active Job object, adding
@@ -130,7 +133,7 @@ module ActiveJob
130
133
  set_callback(:enqueue, :after, *filters, &blk)
131
134
  end
132
135
 
133
- # Defines a callback that will get called around the enqueueing
136
+ # Defines a callback that will get called around the enqueuing
134
137
  # of the job.
135
138
  #
136
139
  # class VideoProcessJob < ActiveJob::Base
@@ -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,11 @@ module ActiveJob
74
84
  @queue_name = self.class.queue_name
75
85
  @priority = self.class.priority
76
86
  @executions = 0
87
+ @exception_executions = {}
77
88
  end
78
89
 
79
90
  # Returns a hash with the job data that can safely be passed to the
80
- # queueing adapter.
91
+ # queuing adapter.
81
92
  def serialize
82
93
  {
83
94
  "job_class" => self.class.name,
@@ -87,7 +98,10 @@ module ActiveJob
87
98
  "priority" => priority,
88
99
  "arguments" => serialize_arguments_if_needed(arguments),
89
100
  "executions" => executions,
90
- "locale" => I18n.locale.to_s
101
+ "exception_executions" => exception_executions,
102
+ "locale" => I18n.locale.to_s,
103
+ "timezone" => Time.zone.try(:name),
104
+ "enqueued_at" => Time.now.utc.iso8601
91
105
  }
92
106
  end
93
107
 
@@ -124,7 +138,10 @@ module ActiveJob
124
138
  self.priority = job_data["priority"]
125
139
  self.serialized_arguments = job_data["arguments"]
126
140
  self.executions = job_data["executions"]
141
+ self.exception_executions = job_data["exception_executions"]
127
142
  self.locale = job_data["locale"] || I18n.locale.to_s
143
+ self.timezone = job_data["timezone"] || Time.zone.try(:name)
144
+ self.enqueued_at = job_data["enqueued_at"]
128
145
  end
129
146
 
130
147
  private
@@ -9,10 +9,12 @@ 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.
@@ -46,14 +48,33 @@ module ActiveJob
46
48
  self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
47
49
  self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
48
50
  self.priority = options[:priority].to_i if options[:priority]
51
+ successfully_enqueued = false
52
+
49
53
  run_callbacks :enqueue do
50
54
  if scheduled_at
51
55
  self.class.queue_adapter.enqueue_at self, scheduled_at
52
56
  else
53
57
  self.class.queue_adapter.enqueue self
54
58
  end
59
+
60
+ successfully_enqueued = true
61
+ end
62
+
63
+ if successfully_enqueued
64
+ self
65
+ else
66
+ if self.class.return_false_on_aborted_enqueue
67
+ false
68
+ else
69
+ ActiveSupport::Deprecation.warn(
70
+ "Rails 6.1 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
71
+ " returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \
72
+ " to remove the deprecations."
73
+ )
74
+
75
+ self
76
+ end
55
77
  end
56
- self
57
78
  end
58
79
  end
59
80
  end
@@ -30,28 +30,36 @@ module ActiveJob
30
30
  # class RemoteServiceJob < ActiveJob::Base
31
31
  # retry_on CustomAppException # defaults to 3s wait, 5 attempts
32
32
  # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
33
+ #
34
+ # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
35
+ # retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
36
+ # # To retry at most 10 times for each individual exception:
37
+ # # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
38
+ # # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10
39
+ #
33
40
  # retry_on(YetAnotherCustomAppException) do |job, error|
34
41
  # ExceptionNotifier.caught(error)
35
42
  # end
36
- # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
37
- # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
38
43
  #
39
44
  # def perform(*args)
40
45
  # # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
41
46
  # # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
42
- # # Might raise Net::OpenTimeout when the remote service is down
47
+ # # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
43
48
  # end
44
49
  # end
45
- def retry_on(exception, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
46
- rescue_from exception do |error|
50
+ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
51
+ rescue_from(*exceptions) do |error|
52
+ executions = executions_for(exceptions)
53
+
47
54
  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
55
+ retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions), queue: queue, priority: priority, error: error
50
56
  else
51
57
  if block_given?
52
- yield self, error
58
+ instrument :retry_stopped, error: error do
59
+ yield self, error
60
+ end
53
61
  else
54
- logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
62
+ instrument :retry_stopped, error: error
55
63
  raise error
56
64
  end
57
65
  end
@@ -76,12 +84,10 @@ module ActiveJob
76
84
  # # Might raise CustomAppException for something domain specific
77
85
  # end
78
86
  # 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}."
87
+ def discard_on(*exceptions)
88
+ rescue_from(*exceptions) do |error|
89
+ instrument :discard, error: error do
90
+ yield self, error if block_given?
85
91
  end
86
92
  end
87
93
  end
@@ -109,11 +115,13 @@ module ActiveJob
109
115
  # end
110
116
  # end
111
117
  def retry_job(options = {})
112
- enqueue options
118
+ instrument :enqueue_retry, options.slice(:error, :wait) do
119
+ enqueue options
120
+ end
113
121
  end
114
122
 
115
123
  private
116
- def determine_delay(seconds_or_duration_or_algorithm)
124
+ def determine_delay(seconds_or_duration_or_algorithm:, executions:)
117
125
  case seconds_or_duration_or_algorithm
118
126
  when :exponentially_longer
119
127
  (executions**4) + 2
@@ -130,5 +138,20 @@ module ActiveJob
130
138
  raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
131
139
  end
132
140
  end
141
+
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)
146
+ end
147
+
148
+ def executions_for(exceptions)
149
+ if exception_executions
150
+ exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
151
+ else
152
+ # Guard against jobs that were persisted before we started having individual executions counters per retry_on
153
+ executions
154
+ end
155
+ end
133
156
  end
134
157
  end
@@ -26,7 +26,7 @@ module ActiveJob
26
26
  end
27
27
  end
28
28
 
29
- # Performs the job immediately. The job is not sent to the queueing adapter
29
+ # Performs the job immediately. The job is not sent to the queuing adapter
30
30
  # but directly executed by blocking the execution of others until it's finished.
31
31
  #
32
32
  # MyJob.new(*args).perform_now
@@ -7,10 +7,10 @@ module ActiveJob
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 4
13
- PRE = "1"
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash/transform_values"
4
3
  require "active_support/core_ext/string/filters"
5
4
  require "active_support/tagged_logging"
6
5
  require "active_support/logger"
@@ -12,13 +11,13 @@ module ActiveJob
12
11
  included do
13
12
  cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
14
13
 
15
- around_enqueue do |_, block, _|
14
+ around_enqueue do |_, block|
16
15
  tag_logger do
17
16
  block.call
18
17
  end
19
18
  end
20
19
 
21
- around_perform do |job, block, _|
20
+ around_perform do |job, block|
22
21
  tag_logger(job.class.name, job.job_id) do
23
22
  payload = { adapter: job.class.queue_adapter, job: job }
24
23
  ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
@@ -28,13 +27,13 @@ module ActiveJob
28
27
  end
29
28
  end
30
29
 
31
- after_enqueue do |job|
30
+ around_enqueue do |job, block|
32
31
  if job.scheduled_at
33
- ActiveSupport::Notifications.instrument "enqueue_at.active_job",
34
- adapter: job.class.queue_adapter, job: job
32
+ ActiveSupport::Notifications.instrument("enqueue_at.active_job",
33
+ adapter: job.class.queue_adapter, job: job, &block)
35
34
  else
36
- ActiveSupport::Notifications.instrument "enqueue.active_job",
37
- adapter: job.class.queue_adapter, job: job
35
+ ActiveSupport::Notifications.instrument("enqueue.active_job",
36
+ adapter: job.class.queue_adapter, job: job, &block)
38
37
  end
39
38
  end
40
39
  end
@@ -71,7 +70,7 @@ module ActiveJob
71
70
  def perform_start(event)
72
71
  info do
73
72
  job = event.payload[:job]
74
- "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
73
+ "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} enqueued at #{job.enqueued_at}" + args_info(job)
75
74
  end
76
75
  end
77
76
 
@@ -89,6 +88,38 @@ module ActiveJob
89
88
  end
90
89
  end
91
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
+
92
123
  private
93
124
  def queue_name(event)
94
125
  event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
@@ -22,6 +22,8 @@ module ActiveJob
22
22
  _queue_adapter
23
23
  end
24
24
 
25
+ # Returns string denoting the name of the configured queue adapter.
26
+ # By default returns +"async"+.
25
27
  def queue_adapter_name
26
28
  _queue_adapter_name
27
29
  end
@@ -3,19 +3,19 @@
3
3
  module ActiveJob
4
4
  # == Active Job adapters
5
5
  #
6
- # Active Job has adapters for the following queueing backends:
6
+ # Active Job has adapters for the following queuing backends:
7
7
  #
8
8
  # * {Backburner}[https://github.com/nesquena/backburner]
9
9
  # * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
10
- # * {Qu}[https://github.com/bkeepers/qu]
11
10
  # * {Que}[https://github.com/chanks/que]
12
11
  # * {queue_classic}[https://github.com/QueueClassic/queue_classic]
13
12
  # * {Resque}[https://github.com/resque/resque]
14
- # * {Sidekiq}[http://sidekiq.org]
13
+ # * {Sidekiq}[https://sidekiq.org]
15
14
  # * {Sneakers}[https://github.com/jondot/sneakers]
16
15
  # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
17
- # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
18
- # * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
16
+ # * {Active Job Async Job}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
17
+ # * {Active Job Inline}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
18
+ # * Please Note: We are not accepting pull requests for new adapters. See the {README}[link:files/activejob/README_md.html] for more details.
19
19
  #
20
20
  # === Backends Features
21
21
  #
@@ -23,7 +23,6 @@ module ActiveJob
23
23
  # |-------------------|-------|--------|------------|------------|---------|---------|
24
24
  # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
25
25
  # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
26
- # | Qu | Yes | Yes | No | No | No | Global |
27
26
  # | Que | Yes | Yes | Yes | Job | No | Job |
28
27
  # | queue_classic | Yes | Yes | Yes* | No | No | No |
29
28
  # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
@@ -53,7 +52,7 @@ module ActiveJob
53
52
  #
54
53
  # No: The adapter will run jobs at the next opportunity and cannot use perform_later.
55
54
  #
56
- # N/A: The adapter does not support queueing.
55
+ # N/A: The adapter does not support queuing.
57
56
  #
58
57
  # NOTE:
59
58
  # queue_classic supports job scheduling since version 3.1.
@@ -75,7 +74,7 @@ module ActiveJob
75
74
  #
76
75
  # No: Does not allow the priority of jobs to be configured.
77
76
  #
78
- # N/A: The adapter does not support queueing, and therefore sorting them.
77
+ # N/A: The adapter does not support queuing, and therefore sorting them.
79
78
  #
80
79
  # ==== Timeout
81
80
  #
@@ -114,7 +113,6 @@ module ActiveJob
114
113
  autoload :InlineAdapter
115
114
  autoload :BackburnerAdapter
116
115
  autoload :DelayedJobAdapter
117
- autoload :QuAdapter
118
116
  autoload :QueAdapter
119
117
  autoload :QueueClassicAdapter
120
118
  autoload :ResqueAdapter
@@ -123,7 +121,7 @@ module ActiveJob
123
121
  autoload :SuckerPunchAdapter
124
122
  autoload :TestAdapter
125
123
 
126
- ADAPTER = "Adapter".freeze
124
+ ADAPTER = "Adapter"
127
125
  private_constant :ADAPTER
128
126
 
129
127
  class << self