activejob 5.2.3.rc1 → 6.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -38
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +12 -6
  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 +33 -21
  10. data/lib/active_job/enqueuing.rb +26 -5
  11. data/lib/active_job/exceptions.rb +33 -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 +39 -8
  15. data/lib/active_job/queue_adapter.rb +2 -0
  16. data/lib/active_job/queue_adapters.rb +5 -7
  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 +1 -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 +262 -56
  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 +17 -9
  36. data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
@@ -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.0 will return false when the enqueing 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,38 @@ 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|
47
- 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
50
+ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
51
+ rescue_from(*exceptions) do |error|
52
+ # Guard against jobs that were persisted before we started having individual executions counters per retry_on
53
+ self.exception_executions ||= {}
54
+ self.exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
55
+
56
+ if exception_executions[exceptions.to_s] < attempts
57
+ retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error
50
58
  else
51
59
  if block_given?
52
- yield self, error
60
+ instrument :retry_stopped, error: error do
61
+ yield self, error
62
+ end
53
63
  else
54
- logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
64
+ instrument :retry_stopped, error: error
55
65
  raise error
56
66
  end
57
67
  end
@@ -76,12 +86,10 @@ module ActiveJob
76
86
  # # Might raise CustomAppException for something domain specific
77
87
  # end
78
88
  # 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}."
89
+ def discard_on(*exceptions)
90
+ rescue_from(*exceptions) do |error|
91
+ instrument :discard, error: error do
92
+ yield self, error if block_given?
85
93
  end
86
94
  end
87
95
  end
@@ -109,7 +117,9 @@ module ActiveJob
109
117
  # end
110
118
  # end
111
119
  def retry_job(options = {})
112
- enqueue options
120
+ instrument :enqueue_retry, options.slice(:error, :wait) do
121
+ enqueue options
122
+ end
113
123
  end
114
124
 
115
125
  private
@@ -130,5 +140,11 @@ module ActiveJob
130
140
  raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
131
141
  end
132
142
  end
143
+
144
+ def instrument(name, error: nil, wait: nil, &block)
145
+ payload = { job: self, adapter: self.class.queue_adapter, error: error, wait: wait }
146
+
147
+ ActiveSupport::Notifications.instrument("#{name}.active_job", payload, &block)
148
+ end
133
149
  end
134
150
  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 = 3
13
- PRE = "rc1"
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "beta1"
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
@@ -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,11 +3,10 @@
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]
@@ -16,6 +15,7 @@ module ActiveJob
16
15
  # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
17
16
  # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
18
17
  # * {Active Job Inline}[http://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
@@ -31,7 +31,7 @@ module ActiveJob
31
31
  # jobs. Since jobs share a single thread pool, long-running jobs will block
32
32
  # short-lived jobs. Fine for dev/test; bad for production.
33
33
  class AsyncAdapter
34
- # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
34
+ # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
35
35
  def initialize(**executor_options)
36
36
  @scheduler = Scheduler.new(**executor_options)
37
37
  end
@@ -16,12 +16,12 @@ module ActiveJob
16
16
  # Rails.application.config.active_job.queue_adapter = :backburner
17
17
  class BackburnerAdapter
18
18
  def enqueue(job) #:nodoc:
19
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name
19
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
20
20
  end
21
21
 
22
22
  def enqueue_at(job, timestamp) #:nodoc:
23
23
  delay = timestamp - Time.current.to_f
24
- Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay
24
+ Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
25
25
  end
26
26
 
27
27
  class JobWrapper #:nodoc:
@@ -16,7 +16,7 @@ module ActiveJob
16
16
  end
17
17
 
18
18
  def enqueue_at(*) #:nodoc:
19
- raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html"
19
+ raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html"
20
20
  end
21
21
  end
22
22
  end
@@ -12,7 +12,7 @@ module ActiveJob
12
12
  #
13
13
  # Rails.application.config.active_job.queue_adapter = :test
14
14
  class TestAdapter
15
- attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject)
15
+ attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter, :reject, :queue)
16
16
  attr_writer(:enqueued_jobs, :performed_jobs)
17
17
 
18
18
  # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
@@ -29,14 +29,14 @@ module ActiveJob
29
29
  return if filtered?(job)
30
30
 
31
31
  job_data = job_to_hash(job)
32
- enqueue_or_perform(perform_enqueued_jobs, job, job_data)
32
+ perform_or_enqueue(perform_enqueued_jobs, job, job_data)
33
33
  end
34
34
 
35
35
  def enqueue_at(job, timestamp) #:nodoc:
36
36
  return if filtered?(job)
37
37
 
38
38
  job_data = job_to_hash(job, at: timestamp)
39
- enqueue_or_perform(perform_enqueued_at_jobs, job, job_data)
39
+ perform_or_enqueue(perform_enqueued_at_jobs, job, job_data)
40
40
  end
41
41
 
42
42
  private
@@ -44,7 +44,7 @@ module ActiveJob
44
44
  { job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras)
45
45
  end
46
46
 
47
- def enqueue_or_perform(perform, job, job_data)
47
+ def perform_or_enqueue(perform, job, job_data)
48
48
  if perform
49
49
  performed_jobs << job_data
50
50
  Base.execute job.serialize
@@ -54,14 +54,28 @@ module ActiveJob
54
54
  end
55
55
 
56
56
  def filtered?(job)
57
+ filtered_queue?(job) || filtered_job_class?(job)
58
+ end
59
+
60
+ def filtered_queue?(job)
61
+ if queue
62
+ job.queue_name != queue.to_s
63
+ end
64
+ end
65
+
66
+ def filtered_job_class?(job)
57
67
  if filter
58
- !Array(filter).include?(job.class)
68
+ !filter_as_proc(filter).call(job)
59
69
  elsif reject
60
- Array(reject).include?(job.class)
61
- else
62
- false
70
+ filter_as_proc(reject).call(job)
63
71
  end
64
72
  end
73
+
74
+ def filter_as_proc(filter)
75
+ return filter if filter.is_a?(Proc)
76
+
77
+ ->(job) { Array(filter).include?(job.class) }
78
+ end
65
79
  end
66
80
  end
67
81
  end
@@ -34,7 +34,7 @@ module ActiveJob
34
34
  end
35
35
 
36
36
  included do
37
- class_attribute :queue_name, instance_accessor: false, default: default_queue_name
37
+ class_attribute :queue_name, instance_accessor: false, default: -> { self.class.default_queue_name }
38
38
  class_attribute :queue_name_delimiter, instance_accessor: false, default: "_"
39
39
  end
40
40
 
@@ -7,17 +7,32 @@ module ActiveJob
7
7
  # = Active Job Railtie
8
8
  class Railtie < Rails::Railtie # :nodoc:
9
9
  config.active_job = ActiveSupport::OrderedOptions.new
10
+ config.active_job.custom_serializers = []
10
11
 
11
12
  initializer "active_job.logger" do
12
13
  ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
13
14
  end
14
15
 
16
+ initializer "active_job.custom_serializers" do |app|
17
+ config.after_initialize do
18
+ custom_serializers = app.config.active_job.delete(:custom_serializers)
19
+ ActiveJob::Serializers.add_serializers custom_serializers
20
+ end
21
+ end
22
+
15
23
  initializer "active_job.set_configs" do |app|
16
24
  options = app.config.active_job
17
25
  options.queue_adapter ||= :async
18
26
 
19
27
  ActiveSupport.on_load(:active_job) do
20
- options.each { |k, v| send("#{k}=", v) }
28
+ options.each do |k, v|
29
+ k = "#{k}="
30
+ send(k, v) if respond_to? k
31
+ end
32
+ end
33
+
34
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
35
+ include ActiveJob::TestHelper
21
36
  end
22
37
  end
23
38