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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -38
- data/MIT-LICENSE +1 -1
- data/README.md +12 -6
- data/lib/active_job.rb +2 -1
- data/lib/active_job/arguments.rb +40 -28
- data/lib/active_job/base.rb +3 -1
- data/lib/active_job/callbacks.rb +4 -1
- data/lib/active_job/core.rb +33 -21
- data/lib/active_job/enqueuing.rb +26 -5
- data/lib/active_job/exceptions.rb +33 -17
- data/lib/active_job/execution.rb +1 -1
- data/lib/active_job/gem_version.rb +4 -4
- data/lib/active_job/logging.rb +39 -8
- data/lib/active_job/queue_adapter.rb +2 -0
- data/lib/active_job/queue_adapters.rb +5 -7
- data/lib/active_job/queue_adapters/async_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/backburner_adapter.rb +2 -2
- data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
- data/lib/active_job/queue_adapters/test_adapter.rb +22 -8
- data/lib/active_job/queue_name.rb +1 -1
- data/lib/active_job/railtie.rb +16 -1
- data/lib/active_job/serializers.rb +63 -0
- data/lib/active_job/serializers/date_serializer.rb +21 -0
- data/lib/active_job/serializers/date_time_serializer.rb +21 -0
- data/lib/active_job/serializers/duration_serializer.rb +24 -0
- data/lib/active_job/serializers/object_serializer.rb +54 -0
- data/lib/active_job/serializers/symbol_serializer.rb +21 -0
- data/lib/active_job/serializers/time_serializer.rb +21 -0
- data/lib/active_job/serializers/time_with_zone_serializer.rb +21 -0
- data/lib/active_job/test_helper.rb +262 -56
- data/lib/active_job/timezones.rb +13 -0
- data/lib/active_job/translation.rb +1 -1
- data/lib/rails/generators/job/job_generator.rb +4 -0
- metadata +17 -9
- data/lib/active_job/queue_adapters/qu_adapter.rb +0 -46
data/lib/active_job/enqueuing.rb
CHANGED
@@ -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.
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
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(
|
46
|
-
rescue_from
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
60
|
+
instrument :retry_stopped, error: error do
|
61
|
+
yield self, error
|
62
|
+
end
|
53
63
|
else
|
54
|
-
|
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(
|
80
|
-
rescue_from
|
81
|
-
|
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
|
-
|
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
|
data/lib/active_job/execution.rb
CHANGED
@@ -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
|
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
|
data/lib/active_job/logging.rb
CHANGED
@@ -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
|
-
|
30
|
+
around_enqueue do |job, block|
|
32
31
|
if job.scheduled_at
|
33
|
-
ActiveSupport::Notifications.instrument
|
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
|
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})"
|
@@ -3,11 +3,10 @@
|
|
3
3
|
module ActiveJob
|
4
4
|
# == Active Job adapters
|
5
5
|
#
|
6
|
-
# Active Job has adapters for the following
|
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
|
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
|
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"
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
!
|
68
|
+
!filter_as_proc(filter).call(job)
|
59
69
|
elsif reject
|
60
|
-
|
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
|
|
data/lib/active_job/railtie.rb
CHANGED
@@ -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
|
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
|
|