activejob 5.2.3

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +126 -0
  5. data/lib/active_job.rb +39 -0
  6. data/lib/active_job/arguments.rb +165 -0
  7. data/lib/active_job/base.rb +74 -0
  8. data/lib/active_job/callbacks.rb +155 -0
  9. data/lib/active_job/configured_job.rb +18 -0
  10. data/lib/active_job/core.rb +158 -0
  11. data/lib/active_job/enqueuing.rb +59 -0
  12. data/lib/active_job/exceptions.rb +134 -0
  13. data/lib/active_job/execution.rb +49 -0
  14. data/lib/active_job/gem_version.rb +17 -0
  15. data/lib/active_job/logging.rb +130 -0
  16. data/lib/active_job/queue_adapter.rb +60 -0
  17. data/lib/active_job/queue_adapters.rb +139 -0
  18. data/lib/active_job/queue_adapters/async_adapter.rb +116 -0
  19. data/lib/active_job/queue_adapters/backburner_adapter.rb +36 -0
  20. data/lib/active_job/queue_adapters/delayed_job_adapter.rb +47 -0
  21. data/lib/active_job/queue_adapters/inline_adapter.rb +23 -0
  22. data/lib/active_job/queue_adapters/qu_adapter.rb +46 -0
  23. data/lib/active_job/queue_adapters/que_adapter.rb +39 -0
  24. data/lib/active_job/queue_adapters/queue_classic_adapter.rb +58 -0
  25. data/lib/active_job/queue_adapters/resque_adapter.rb +53 -0
  26. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +47 -0
  27. data/lib/active_job/queue_adapters/sneakers_adapter.rb +48 -0
  28. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +49 -0
  29. data/lib/active_job/queue_adapters/test_adapter.rb +67 -0
  30. data/lib/active_job/queue_name.rb +49 -0
  31. data/lib/active_job/queue_priority.rb +43 -0
  32. data/lib/active_job/railtie.rb +34 -0
  33. data/lib/active_job/test_case.rb +11 -0
  34. data/lib/active_job/test_helper.rb +456 -0
  35. data/lib/active_job/translation.rb +13 -0
  36. data/lib/active_job/version.rb +10 -0
  37. data/lib/rails/generators/job/job_generator.rb +40 -0
  38. data/lib/rails/generators/job/templates/application_job.rb.tt +9 -0
  39. data/lib/rails/generators/job/templates/job.rb.tt +9 -0
  40. metadata +110 -0
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+
5
+ module ActiveJob
6
+ # = Active Job Callbacks
7
+ #
8
+ # Active Job provides hooks during the life cycle of a job. Callbacks allow you
9
+ # to trigger logic during this cycle. Available callbacks are:
10
+ #
11
+ # * <tt>before_enqueue</tt>
12
+ # * <tt>around_enqueue</tt>
13
+ # * <tt>after_enqueue</tt>
14
+ # * <tt>before_perform</tt>
15
+ # * <tt>around_perform</tt>
16
+ # * <tt>after_perform</tt>
17
+ #
18
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
19
+ #
20
+ module Callbacks
21
+ extend ActiveSupport::Concern
22
+ include ActiveSupport::Callbacks
23
+
24
+ class << self
25
+ include ActiveSupport::Callbacks
26
+ define_callbacks :execute
27
+ end
28
+
29
+ included do
30
+ define_callbacks :perform
31
+ define_callbacks :enqueue
32
+ end
33
+
34
+ # These methods will be included into any Active Job object, adding
35
+ # callbacks for +perform+ and +enqueue+ methods.
36
+ module ClassMethods
37
+ # Defines a callback that will get called right before the
38
+ # job's perform method is executed.
39
+ #
40
+ # class VideoProcessJob < ActiveJob::Base
41
+ # queue_as :default
42
+ #
43
+ # before_perform do |job|
44
+ # UserMailer.notify_video_started_processing(job.arguments.first)
45
+ # end
46
+ #
47
+ # def perform(video_id)
48
+ # Video.find(video_id).process
49
+ # end
50
+ # end
51
+ #
52
+ def before_perform(*filters, &blk)
53
+ set_callback(:perform, :before, *filters, &blk)
54
+ end
55
+
56
+ # Defines a callback that will get called right after the
57
+ # job's perform method has finished.
58
+ #
59
+ # class VideoProcessJob < ActiveJob::Base
60
+ # queue_as :default
61
+ #
62
+ # after_perform do |job|
63
+ # UserMailer.notify_video_processed(job.arguments.first)
64
+ # end
65
+ #
66
+ # def perform(video_id)
67
+ # Video.find(video_id).process
68
+ # end
69
+ # end
70
+ #
71
+ def after_perform(*filters, &blk)
72
+ set_callback(:perform, :after, *filters, &blk)
73
+ end
74
+
75
+ # Defines a callback that will get called around the job's perform method.
76
+ #
77
+ # class VideoProcessJob < ActiveJob::Base
78
+ # queue_as :default
79
+ #
80
+ # around_perform do |job, block|
81
+ # UserMailer.notify_video_started_processing(job.arguments.first)
82
+ # block.call
83
+ # UserMailer.notify_video_processed(job.arguments.first)
84
+ # end
85
+ #
86
+ # def perform(video_id)
87
+ # Video.find(video_id).process
88
+ # end
89
+ # end
90
+ #
91
+ def around_perform(*filters, &blk)
92
+ set_callback(:perform, :around, *filters, &blk)
93
+ end
94
+
95
+ # Defines a callback that will get called right before the
96
+ # job is enqueued.
97
+ #
98
+ # class VideoProcessJob < ActiveJob::Base
99
+ # queue_as :default
100
+ #
101
+ # before_enqueue do |job|
102
+ # $statsd.increment "enqueue-video-job.try"
103
+ # end
104
+ #
105
+ # def perform(video_id)
106
+ # Video.find(video_id).process
107
+ # end
108
+ # end
109
+ #
110
+ def before_enqueue(*filters, &blk)
111
+ set_callback(:enqueue, :before, *filters, &blk)
112
+ end
113
+
114
+ # Defines a callback that will get called right after the
115
+ # job is enqueued.
116
+ #
117
+ # class VideoProcessJob < ActiveJob::Base
118
+ # queue_as :default
119
+ #
120
+ # after_enqueue do |job|
121
+ # $statsd.increment "enqueue-video-job.success"
122
+ # end
123
+ #
124
+ # def perform(video_id)
125
+ # Video.find(video_id).process
126
+ # end
127
+ # end
128
+ #
129
+ def after_enqueue(*filters, &blk)
130
+ set_callback(:enqueue, :after, *filters, &blk)
131
+ end
132
+
133
+ # Defines a callback that will get called around the enqueueing
134
+ # of the job.
135
+ #
136
+ # class VideoProcessJob < ActiveJob::Base
137
+ # queue_as :default
138
+ #
139
+ # around_enqueue do |job, block|
140
+ # $statsd.time "video-job.process" do
141
+ # block.call
142
+ # end
143
+ # end
144
+ #
145
+ # def perform(video_id)
146
+ # Video.find(video_id).process
147
+ # end
148
+ # end
149
+ #
150
+ def around_enqueue(*filters, &blk)
151
+ set_callback(:enqueue, :around, *filters, &blk)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ class ConfiguredJob #:nodoc:
5
+ def initialize(job_class, options = {})
6
+ @options = options
7
+ @job_class = job_class
8
+ end
9
+
10
+ def perform_now(*args)
11
+ @job_class.new(*args).perform_now
12
+ end
13
+
14
+ def perform_later(*args)
15
+ @job_class.new(*args).enqueue @options
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ # Provides general behavior that will be included into every Active Job
5
+ # object that inherits from ActiveJob::Base.
6
+ module Core
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Job arguments
11
+ attr_accessor :arguments
12
+ attr_writer :serialized_arguments
13
+
14
+ # Timestamp when the job should be performed
15
+ attr_accessor :scheduled_at
16
+
17
+ # Job Identifier
18
+ attr_accessor :job_id
19
+
20
+ # Queue in which the job will reside.
21
+ attr_writer :queue_name
22
+
23
+ # Priority that the job will have (lower is more priority).
24
+ attr_writer :priority
25
+
26
+ # ID optionally provided by adapter
27
+ attr_accessor :provider_job_id
28
+
29
+ # Number of times this job has been executed (which increments on every retry, like after an exception).
30
+ attr_accessor :executions
31
+
32
+ # I18n.locale to be used during the job.
33
+ attr_accessor :locale
34
+ end
35
+
36
+ # These methods will be included into any Active Job object, adding
37
+ # helpers for de/serialization and creation of job instances.
38
+ module ClassMethods
39
+ # Creates a new job instance from a hash created with +serialize+
40
+ def deserialize(job_data)
41
+ job = job_data["job_class"].constantize.new
42
+ job.deserialize(job_data)
43
+ job
44
+ end
45
+
46
+ # Creates a job preconfigured with the given options. You can call
47
+ # perform_later with the job arguments to enqueue the job with the
48
+ # preconfigured options
49
+ #
50
+ # ==== Options
51
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay
52
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
53
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
54
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
55
+ #
56
+ # ==== Examples
57
+ #
58
+ # VideoJob.set(queue: :some_queue).perform_later(Video.last)
59
+ # VideoJob.set(wait: 5.minutes).perform_later(Video.last)
60
+ # VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last)
61
+ # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
62
+ # VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last)
63
+ # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last)
64
+ def set(options = {})
65
+ ConfiguredJob.new(self, options)
66
+ end
67
+ end
68
+
69
+ # Creates a new job instance. Takes the arguments that will be
70
+ # passed to the perform method.
71
+ def initialize(*arguments)
72
+ @arguments = arguments
73
+ @job_id = SecureRandom.uuid
74
+ @queue_name = self.class.queue_name
75
+ @priority = self.class.priority
76
+ @executions = 0
77
+ end
78
+
79
+ # Returns a hash with the job data that can safely be passed to the
80
+ # queueing adapter.
81
+ def serialize
82
+ {
83
+ "job_class" => self.class.name,
84
+ "job_id" => job_id,
85
+ "provider_job_id" => provider_job_id,
86
+ "queue_name" => queue_name,
87
+ "priority" => priority,
88
+ "arguments" => serialize_arguments_if_needed(arguments),
89
+ "executions" => executions,
90
+ "locale" => I18n.locale.to_s
91
+ }
92
+ end
93
+
94
+ # Attaches the stored job data to the current instance. Receives a hash
95
+ # returned from +serialize+
96
+ #
97
+ # ==== Examples
98
+ #
99
+ # class DeliverWebhookJob < ActiveJob::Base
100
+ # attr_writer :attempt_number
101
+ #
102
+ # def attempt_number
103
+ # @attempt_number ||= 0
104
+ # end
105
+ #
106
+ # def serialize
107
+ # super.merge('attempt_number' => attempt_number + 1)
108
+ # end
109
+ #
110
+ # def deserialize(job_data)
111
+ # super
112
+ # self.attempt_number = job_data['attempt_number']
113
+ # end
114
+ #
115
+ # rescue_from(Timeout::Error) do |exception|
116
+ # raise exception if attempt_number > 5
117
+ # retry_job(wait: 10)
118
+ # end
119
+ # end
120
+ def deserialize(job_data)
121
+ self.job_id = job_data["job_id"]
122
+ self.provider_job_id = job_data["provider_job_id"]
123
+ self.queue_name = job_data["queue_name"]
124
+ self.priority = job_data["priority"]
125
+ self.serialized_arguments = job_data["arguments"]
126
+ self.executions = job_data["executions"]
127
+ self.locale = job_data["locale"] || I18n.locale.to_s
128
+ end
129
+
130
+ private
131
+ def serialize_arguments_if_needed(arguments)
132
+ if arguments_serialized?
133
+ @serialized_arguments
134
+ else
135
+ serialize_arguments(arguments)
136
+ end
137
+ end
138
+
139
+ def deserialize_arguments_if_needed
140
+ if arguments_serialized?
141
+ @arguments = deserialize_arguments(@serialized_arguments)
142
+ @serialized_arguments = nil
143
+ end
144
+ end
145
+
146
+ def serialize_arguments(arguments)
147
+ Arguments.serialize(arguments)
148
+ end
149
+
150
+ def deserialize_arguments(serialized_args)
151
+ Arguments.deserialize(serialized_args)
152
+ end
153
+
154
+ def arguments_serialized?
155
+ defined?(@serialized_arguments) && @serialized_arguments
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/arguments"
4
+
5
+ module ActiveJob
6
+ # Provides behavior for enqueuing jobs.
7
+ module Enqueuing
8
+ extend ActiveSupport::Concern
9
+
10
+ # Includes the +perform_later+ method for job initialization.
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.
16
+ #
17
+ # Returns an instance of the job class queued with arguments available in
18
+ # Job#arguments.
19
+ def perform_later(*args)
20
+ job_or_instantiate(*args).enqueue
21
+ end
22
+
23
+ private
24
+ def job_or_instantiate(*args) # :doc:
25
+ args.first.is_a?(self) ? args.first : new(*args)
26
+ end
27
+ end
28
+
29
+ # Enqueues the job to be performed by the queue adapter.
30
+ #
31
+ # ==== Options
32
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay
33
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
34
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
35
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
36
+ #
37
+ # ==== Examples
38
+ #
39
+ # my_job_instance.enqueue
40
+ # my_job_instance.enqueue wait: 5.minutes
41
+ # my_job_instance.enqueue queue: :important
42
+ # my_job_instance.enqueue wait_until: Date.tomorrow.midnight
43
+ # my_job_instance.enqueue priority: 10
44
+ def enqueue(options = {})
45
+ self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
46
+ self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
47
+ self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
48
+ self.priority = options[:priority].to_i if options[:priority]
49
+ run_callbacks :enqueue do
50
+ if scheduled_at
51
+ self.class.queue_adapter.enqueue_at self, scheduled_at
52
+ else
53
+ self.class.queue_adapter.enqueue self
54
+ end
55
+ end
56
+ self
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/numeric/time"
4
+
5
+ module ActiveJob
6
+ # Provides behavior for retrying and discarding jobs on exceptions.
7
+ module Exceptions
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ # Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts.
12
+ # If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to
13
+ # bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a
14
+ # holding queue for inspection.
15
+ #
16
+ # You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting
17
+ # the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.
18
+ #
19
+ # ==== Options
20
+ # * <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)
24
+ # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts)
25
+ # * <tt>:queue</tt> - Re-enqueues the job on a different queue
26
+ # * <tt>:priority</tt> - Re-enqueues the job with a different priority
27
+ #
28
+ # ==== Examples
29
+ #
30
+ # class RemoteServiceJob < ActiveJob::Base
31
+ # retry_on CustomAppException # defaults to 3s wait, 5 attempts
32
+ # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
33
+ # retry_on(YetAnotherCustomAppException) do |job, error|
34
+ # ExceptionNotifier.caught(error)
35
+ # end
36
+ # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
37
+ # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
38
+ #
39
+ # def perform(*args)
40
+ # # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
41
+ # # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
42
+ # # Might raise Net::OpenTimeout when the remote service is down
43
+ # end
44
+ # 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
+ else
51
+ if block_given?
52
+ yield self, error
53
+ else
54
+ logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
55
+ raise error
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
62
+ # like an Active Record, is no longer available, and the job is thus no longer relevant.
63
+ #
64
+ # You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.
65
+ #
66
+ # ==== Example
67
+ #
68
+ # class SearchIndexingJob < ActiveJob::Base
69
+ # discard_on ActiveJob::DeserializationError
70
+ # discard_on(CustomAppException) do |job, error|
71
+ # ExceptionNotifier.caught(error)
72
+ # end
73
+ #
74
+ # def perform(record)
75
+ # # Will raise ActiveJob::DeserializationError if the record can't be deserialized
76
+ # # Might raise CustomAppException for something domain specific
77
+ # end
78
+ # 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}."
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # Reschedules the job to be re-executed. This is useful in combination
91
+ # with the +rescue_from+ option. When you rescue an exception from your job
92
+ # you can ask Active Job to retry performing your job.
93
+ #
94
+ # ==== Options
95
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay in seconds
96
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
97
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
98
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
99
+ #
100
+ # ==== Examples
101
+ #
102
+ # class SiteScraperJob < ActiveJob::Base
103
+ # rescue_from(ErrorLoadingSite) do
104
+ # retry_job queue: :low_priority
105
+ # end
106
+ #
107
+ # def perform(*args)
108
+ # # raise ErrorLoadingSite if cannot scrape
109
+ # end
110
+ # end
111
+ def retry_job(options = {})
112
+ enqueue options
113
+ end
114
+
115
+ private
116
+ def determine_delay(seconds_or_duration_or_algorithm)
117
+ case seconds_or_duration_or_algorithm
118
+ 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
126
+ when Proc
127
+ algorithm = seconds_or_duration_or_algorithm
128
+ algorithm.call(executions)
129
+ else
130
+ raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
131
+ end
132
+ end
133
+ end
134
+ end