activejob 7.1.5.2 → 8.1.2.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -264
  3. data/README.md +8 -6
  4. data/lib/active_job/arguments.rb +51 -59
  5. data/lib/active_job/base.rb +5 -6
  6. data/lib/active_job/callbacks.rb +0 -3
  7. data/lib/active_job/configured_job.rb +5 -4
  8. data/lib/active_job/continuable.rb +102 -0
  9. data/lib/active_job/continuation/step.rb +83 -0
  10. data/lib/active_job/continuation/test_helper.rb +89 -0
  11. data/lib/active_job/continuation/validation.rb +50 -0
  12. data/lib/active_job/continuation.rb +332 -0
  13. data/lib/active_job/core.rb +25 -22
  14. data/lib/active_job/enqueue_after_transaction_commit.rb +38 -0
  15. data/lib/active_job/enqueuing.rb +46 -12
  16. data/lib/active_job/exceptions.rb +21 -17
  17. data/lib/active_job/execution_state.rb +11 -0
  18. data/lib/active_job/gem_version.rb +3 -3
  19. data/lib/active_job/instrumentation.rb +12 -12
  20. data/lib/active_job/log_subscriber.rb +65 -6
  21. data/lib/active_job/logging.rb +16 -2
  22. data/lib/active_job/queue_adapter.rb +6 -4
  23. data/lib/active_job/queue_adapters/abstract_adapter.rb +25 -0
  24. data/lib/active_job/queue_adapters/async_adapter.rb +7 -3
  25. data/lib/active_job/queue_adapters/backburner_adapter.rb +1 -1
  26. data/lib/active_job/queue_adapters/delayed_job_adapter.rb +1 -1
  27. data/lib/active_job/queue_adapters/inline_adapter.rb +1 -1
  28. data/lib/active_job/queue_adapters/queue_classic_adapter.rb +1 -1
  29. data/lib/active_job/queue_adapters/resque_adapter.rb +1 -1
  30. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +20 -1
  31. data/lib/active_job/queue_adapters/sneakers_adapter.rb +1 -1
  32. data/lib/active_job/queue_adapters/test_adapter.rb +6 -2
  33. data/lib/active_job/queue_adapters.rb +1 -4
  34. data/lib/active_job/railtie.rb +25 -4
  35. data/lib/active_job/serializers/action_controller_parameters_serializer.rb +25 -0
  36. data/lib/active_job/serializers/big_decimal_serializer.rb +3 -4
  37. data/lib/active_job/serializers/date_serializer.rb +3 -4
  38. data/lib/active_job/serializers/date_time_serializer.rb +3 -4
  39. data/lib/active_job/serializers/duration_serializer.rb +5 -6
  40. data/lib/active_job/serializers/module_serializer.rb +3 -4
  41. data/lib/active_job/serializers/object_serializer.rb +13 -14
  42. data/lib/active_job/serializers/range_serializer.rb +9 -9
  43. data/lib/active_job/serializers/symbol_serializer.rb +4 -5
  44. data/lib/active_job/serializers/time_serializer.rb +3 -4
  45. data/lib/active_job/serializers/time_with_zone_serializer.rb +3 -4
  46. data/lib/active_job/serializers.rb +62 -18
  47. data/lib/active_job/structured_event_subscriber.rb +220 -0
  48. data/lib/active_job/test_helper.rb +28 -5
  49. data/lib/active_job.rb +5 -9
  50. metadata +18 -11
  51. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -49
  52. data/lib/active_job/timezones.rb +0 -13
  53. data/lib/active_job/translation.rb +0 -13
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveJob
4
+ # Raised during job payload deserialization when it references an uninitialized job class.
5
+ class UnknownJobClassError < NameError
6
+ def initialize(job_class_name)
7
+ super("Failed to instantiate job, class `#{job_class_name}` doesn't exist", job_class_name)
8
+ end
9
+ end
10
+
4
11
  # = Active Job \Core
5
12
  #
6
13
  # Provides general behavior that will be included into every Active Job
@@ -13,9 +20,7 @@ module ActiveJob
13
20
  attr_writer :serialized_arguments
14
21
 
15
22
  # Time when the job should be performed
16
- attr_reader :scheduled_at
17
-
18
- attr_reader :_scheduled_at_time # :nodoc:
23
+ attr_accessor :scheduled_at
19
24
 
20
25
  # Job Identifier
21
26
  attr_accessor :job_id
@@ -62,7 +67,10 @@ module ActiveJob
62
67
  module ClassMethods
63
68
  # Creates a new job instance from a hash created with +serialize+
64
69
  def deserialize(job_data)
65
- job = job_data["job_class"].constantize.new
70
+ job_class = job_data["job_class"].safe_constantize
71
+ raise UnknownJobClassError, job_data["job_class"] unless job_class
72
+
73
+ job = job_class.new
66
74
  job.deserialize(job_data)
67
75
  job
68
76
  end
@@ -97,7 +105,6 @@ module ActiveJob
97
105
  @job_id = SecureRandom.uuid
98
106
  @queue_name = self.class.queue_name
99
107
  @scheduled_at = nil
100
- @_scheduled_at_time = nil
101
108
  @priority = self.class.priority
102
109
  @executions = 0
103
110
  @exception_executions = {}
@@ -117,10 +124,10 @@ module ActiveJob
117
124
  "arguments" => serialize_arguments_if_needed(arguments),
118
125
  "executions" => executions,
119
126
  "exception_executions" => exception_executions,
120
- "locale" => I18n.locale.to_s,
127
+ "locale" => locale || I18n.locale.to_s,
121
128
  "timezone" => timezone,
122
129
  "enqueued_at" => Time.now.utc.iso8601(9),
123
- "scheduled_at" => _scheduled_at_time ? _scheduled_at_time.utc.iso8601(9) : nil,
130
+ "scheduled_at" => scheduled_at ? scheduled_at.utc.iso8601(9) : nil,
124
131
  }
125
132
  end
126
133
 
@@ -160,8 +167,8 @@ module ActiveJob
160
167
  self.exception_executions = job_data["exception_executions"]
161
168
  self.locale = job_data["locale"] || I18n.locale.to_s
162
169
  self.timezone = job_data["timezone"] || Time.zone&.name
163
- self.enqueued_at = Time.iso8601(job_data["enqueued_at"]) if job_data["enqueued_at"]
164
- self.scheduled_at = Time.iso8601(job_data["scheduled_at"]) if job_data["scheduled_at"]
170
+ self.enqueued_at = deserialize_time(job_data["enqueued_at"]) if job_data["enqueued_at"]
171
+ self.scheduled_at = deserialize_time(job_data["scheduled_at"]) if job_data["scheduled_at"]
165
172
  end
166
173
 
167
174
  # Configures the job with the given options.
@@ -174,18 +181,6 @@ module ActiveJob
174
181
  self
175
182
  end
176
183
 
177
- def scheduled_at=(value)
178
- @_scheduled_at_time = if value&.is_a?(Numeric)
179
- ActiveJob.deprecator.warn(<<~MSG.squish)
180
- Assigning a numeric/epoch value to scheduled_at is deprecated. Use a Time object instead.
181
- MSG
182
- Time.at(value)
183
- else
184
- value
185
- end
186
- @scheduled_at = value
187
- end
188
-
189
184
  private
190
185
  def serialize_arguments_if_needed(arguments)
191
186
  if arguments_serialized?
@@ -211,7 +206,15 @@ module ActiveJob
211
206
  end
212
207
 
213
208
  def arguments_serialized?
214
- defined?(@serialized_arguments) && @serialized_arguments
209
+ @serialized_arguments
210
+ end
211
+
212
+ def deserialize_time(time)
213
+ if time.is_a?(Time)
214
+ time
215
+ else
216
+ Time.iso8601(time)
217
+ end
215
218
  end
216
219
  end
217
220
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module EnqueueAfterTransactionCommit # :nodoc:
5
+ class << self
6
+ def included(base)
7
+ ActiveJob.singleton_class.prepend(ActiveJobMethods)
8
+ end
9
+ end
10
+
11
+ module ActiveJobMethods
12
+ # Ensures perform_all_later respects each job's enqueue_after_transaction_commit configuration.
13
+ # Jobs with enqueue_after_transaction_commit set to true are deferred and enqueued only after the transaction commits;
14
+ # other jobs are enqueued immediately. This ensures enqueuing timing matches the per-job setting.
15
+ def perform_all_later(*jobs)
16
+ jobs.flatten!
17
+ deferred_jobs, immediate_jobs = jobs.partition { |job| job.class.enqueue_after_transaction_commit }
18
+ super(immediate_jobs) if immediate_jobs.any?
19
+ ActiveRecord.after_all_transactions_commit { super(deferred_jobs) } if deferred_jobs.any?
20
+ nil
21
+ end
22
+ end
23
+
24
+ private
25
+ def raw_enqueue
26
+ if self.class.enqueue_after_transaction_commit
27
+ self.successfully_enqueued = true
28
+ ActiveRecord.after_all_transactions_commit do
29
+ self.successfully_enqueued = false
30
+ super
31
+ end
32
+ self
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -21,7 +21,7 @@ module ActiveJob
21
21
  adapter_jobs.each do |job|
22
22
  job.successfully_enqueued = false
23
23
  if job.scheduled_at
24
- queue_adapter.enqueue_at(job, job._scheduled_at_time.to_f)
24
+ queue_adapter.enqueue_at(job, job.scheduled_at.to_f)
25
25
  else
26
26
  queue_adapter.enqueue(job)
27
27
  end
@@ -40,6 +40,19 @@ module ActiveJob
40
40
  module Enqueuing
41
41
  extend ActiveSupport::Concern
42
42
 
43
+ included do
44
+ ##
45
+ # :singleton-method:
46
+ #
47
+ # Defines if enqueueing this job from inside an Active Record transaction
48
+ # automatically defers the enqueue to after the transaction commits.
49
+ #
50
+ # It can be set on a per job basis:
51
+ # - true forces the job to be deferred.
52
+ # - false forces the job to be queued immediately.
53
+ class_attribute :enqueue_after_transaction_commit, instance_accessor: false, instance_predicate: false, default: false
54
+ end
55
+
43
56
  # Includes the +perform_later+ method for job initialization.
44
57
  module ClassMethods
45
58
  # Push a job onto the queue. By default the arguments must be either String,
@@ -50,9 +63,21 @@ module ActiveJob
50
63
  # custom serializers.
51
64
  #
52
65
  # Returns an instance of the job class queued with arguments available in
53
- # Job#arguments or false if the enqueue did not succeed.
66
+ # Job#arguments or +false+ if the enqueue did not succeed.
54
67
  #
55
68
  # After the attempted enqueue, the job will be yielded to an optional block.
69
+ #
70
+ # If Active Job is used conjointly with Active Record, and #perform_later is called
71
+ # inside an Active Record transaction, then the enqueue is implicitly deferred to after
72
+ # the transaction is committed, or dropped if it's rolled back. In such case #perform_later
73
+ # will return the job instance like if it was successfully enqueued, but will still return
74
+ # +false+ if a callback prevented the job from being enqueued.
75
+ #
76
+ # This behavior can be changed on a per job basis:
77
+ #
78
+ # class NotificationJob < ApplicationJob
79
+ # self.enqueue_after_transaction_commit = false
80
+ # end
56
81
  def perform_later(...)
57
82
  job = job_or_instantiate(...)
58
83
  enqueue_result = job.enqueue
@@ -63,7 +88,7 @@ module ActiveJob
63
88
  end
64
89
 
65
90
  private
66
- def job_or_instantiate(*args) # :doc:
91
+ def job_or_instantiate(*args, &) # :doc:
67
92
  args.first.is_a?(self) ? args.first : new(*args)
68
93
  end
69
94
  ruby2_keywords(:job_or_instantiate)
@@ -88,9 +113,25 @@ module ActiveJob
88
113
  set(options)
89
114
  self.successfully_enqueued = false
90
115
 
91
- run_callbacks :enqueue do
116
+ raw_enqueue
117
+
118
+ if successfully_enqueued?
119
+ self
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ private
126
+ def raw_enqueue
127
+ run_callbacks :enqueue do
128
+ _raw_enqueue
129
+ end
130
+ end
131
+
132
+ def _raw_enqueue
92
133
  if scheduled_at
93
- queue_adapter.enqueue_at self, _scheduled_at_time.to_f
134
+ queue_adapter.enqueue_at self, scheduled_at.to_f
94
135
  else
95
136
  queue_adapter.enqueue self
96
137
  end
@@ -99,12 +140,5 @@ module ActiveJob
99
140
  rescue EnqueueError => e
100
141
  self.enqueue_error = e
101
142
  end
102
-
103
- if successfully_enqueued?
104
- self
105
- else
106
- false
107
- end
108
- end
109
143
  end
110
144
  end
@@ -21,7 +21,7 @@ module ActiveJob
21
21
  # You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting
22
22
  # the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.
23
23
  #
24
- # `retry_on` and `discard_on` handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
24
+ # +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
25
25
  # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if any.
26
26
  #
27
27
  # ==== Options
@@ -29,11 +29,12 @@ module ActiveJob
29
29
  # as a computing proc that takes the number of executions so far as an argument, or as a symbol reference of
30
30
  # <tt>:polynomially_longer</tt>, which applies the wait algorithm of <tt>((executions**4) + (Kernel.rand * (executions**4) * jitter)) + 2</tt>
31
31
  # (first wait ~3s, then ~18s, then ~83s, etc)
32
- # * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts) or a symbol reference of <tt>:unlimited</tt>
33
- # to retry the job until it succeeds
32
+ # * <tt>:attempts</tt> - Enqueues the job the specified number of times (default: 5 attempts) or a symbol reference of <tt>:unlimited</tt>
33
+ # to retry the job until it succeeds. The number of attempts includes the original job execution.
34
34
  # * <tt>:queue</tt> - Re-enqueues the job on a different queue
35
35
  # * <tt>:priority</tt> - Re-enqueues the job with a different priority
36
36
  # * <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)
37
+ # * <tt>:report</tt> - Errors will be reported to the Rails.error reporter before being retried
37
38
  #
38
39
  # ==== Examples
39
40
  #
@@ -49,8 +50,9 @@ module ActiveJob
49
50
  # # retry_on Net::ReadTimeout, wait: 5.seconds, jitter: 0.30, attempts: 10
50
51
  # # retry_on Timeout::Error, wait: :polynomially_longer, attempts: 10
51
52
  #
52
- # retry_on(YetAnotherCustomAppException) do |job, error|
53
- # ExceptionNotifier.caught(error)
53
+ # retry_on YetAnotherCustomAppException, report: true
54
+ # retry_on EvenWorseCustomAppException do |job, error|
55
+ # CustomErrorHandlingCode.handle(job, error)
54
56
  # end
55
57
  #
56
58
  # def perform(*args)
@@ -59,16 +61,11 @@ module ActiveJob
59
61
  # # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
60
62
  # end
61
63
  # end
62
- def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT)
63
- if wait == :exponentially_longer
64
- ActiveJob.deprecator.warn(<<~MSG.squish)
65
- `wait: :exponentially_longer` will actually wait polynomially longer and is therefore deprecated.
66
- Prefer `wait: :polynomially_longer` to avoid confusion and keep the same behavior.
67
- MSG
68
- end
64
+ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT, report: false)
69
65
  rescue_from(*exceptions) do |error|
70
66
  executions = executions_for(exceptions)
71
67
  if attempts == :unlimited || executions < attempts
68
+ ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
72
69
  retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error
73
70
  else
74
71
  if block_given?
@@ -88,17 +85,20 @@ module ActiveJob
88
85
  # Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
89
86
  # like an Active Record, is no longer available, and the job is thus no longer relevant.
90
87
  #
88
+ # Passing the <tt>:report</tt> option reports the error through the error reporter before discarding the job.
89
+ #
91
90
  # 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.
92
91
  #
93
- # `retry_on` and `discard_on` handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
92
+ # +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
94
93
  # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if any.
95
94
  #
96
95
  # ==== Example
97
96
  #
98
97
  # class SearchIndexingJob < ActiveJob::Base
99
98
  # discard_on ActiveJob::DeserializationError
100
- # discard_on(CustomAppException) do |job, error|
101
- # ExceptionNotifier.caught(error)
99
+ # discard_on CustomAppException, report: true
100
+ # discard_on(AnotherCustomAppException) do |job, error|
101
+ # CustomErrorHandlingCode.handle(job, error)
102
102
  # end
103
103
  #
104
104
  # def perform(record)
@@ -106,9 +106,10 @@ module ActiveJob
106
106
  # # Might raise CustomAppException for something domain specific
107
107
  # end
108
108
  # end
109
- def discard_on(*exceptions)
109
+ def discard_on(*exceptions, report: false)
110
110
  rescue_from(*exceptions) do |error|
111
111
  instrument :discard, error: error do
112
+ ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
112
113
  yield self, error if block_given?
113
114
  run_after_discard_procs(error)
114
115
  end
@@ -156,7 +157,10 @@ module ActiveJob
156
157
  # end
157
158
  def retry_job(options = {})
158
159
  instrument :enqueue_retry, options.slice(:error, :wait) do
160
+ scheduled_at, queue_name, priority = self.scheduled_at, self.queue_name, self.priority
159
161
  enqueue options
162
+ ensure
163
+ self.scheduled_at, self.queue_name, self.priority = scheduled_at, queue_name, priority
160
164
  end
161
165
  end
162
166
 
@@ -168,7 +172,7 @@ module ActiveJob
168
172
  jitter = jitter == JITTER_DEFAULT ? self.class.retry_jitter : (jitter || 0.0)
169
173
 
170
174
  case seconds_or_duration_or_algorithm
171
- when :exponentially_longer, :polynomially_longer
175
+ when :polynomially_longer
172
176
  # This delay uses a polynomial backoff strategy, which was previously misnamed as exponential
173
177
  delay = executions**4
174
178
  delay_jitter = determine_jitter_for_delay(delay, jitter)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module ExecutionState # :nodoc:
5
+ def perform_now
6
+ I18n.with_locale(locale) do
7
+ Time.use_zone(timezone) { super }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -7,10 +7,10 @@ module ActiveJob
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
10
+ MAJOR = 8
11
11
  MINOR = 1
12
- TINY = 5
13
- PRE = "2"
12
+ TINY = 2
13
+ PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -26,24 +26,24 @@ module ActiveJob
26
26
  instrument(:perform) { super }
27
27
  end
28
28
 
29
+ def instrument(operation, payload = {}, &block) # :nodoc:
30
+ payload[:job] = self
31
+ payload[:adapter] = queue_adapter
32
+
33
+ ActiveSupport::Notifications.instrument("#{operation}.active_job", payload) do |payload|
34
+ value = block.call(payload) if block
35
+ payload[:aborted] = @_halted_callback_hook_called if defined?(@_halted_callback_hook_called)
36
+ @_halted_callback_hook_called = nil
37
+ value
38
+ end
39
+ end
40
+
29
41
  private
30
42
  def _perform_job
31
43
  instrument(:perform_start)
32
44
  super
33
45
  end
34
46
 
35
- def instrument(operation, payload = {}, &block)
36
- payload[:job] = self
37
- payload[:adapter] = queue_adapter
38
-
39
- ActiveSupport::Notifications.instrument("#{operation}.active_job", payload) do
40
- value = block.call if block
41
- payload[:aborted] = @_halted_callback_hook_called if defined?(@_halted_callback_hook_called)
42
- @_halted_callback_hook_called = nil
43
- value
44
- end
45
- end
46
-
47
47
  def halted_callback_hook(*)
48
48
  super
49
49
  @_halted_callback_hook_called = true
@@ -50,7 +50,7 @@ module ActiveJob
50
50
  info do
51
51
  jobs = event.payload[:jobs]
52
52
  adapter = event.payload[:adapter]
53
- enqueued_count = event.payload[:enqueued_count]
53
+ enqueued_count = event.payload[:enqueued_count].to_i
54
54
 
55
55
  if enqueued_count == jobs.size
56
56
  enqueued_jobs_message(adapter, jobs)
@@ -87,8 +87,9 @@ module ActiveJob
87
87
  job = event.payload[:job]
88
88
  ex = event.payload[:exception_object]
89
89
  if ex
90
+ cleaned_backtrace = backtrace_cleaner.clean(ex.backtrace)
90
91
  error do
91
- "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")
92
+ "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(cleaned_backtrace).join("\n")
92
93
  end
93
94
  elsif event.payload[:aborted]
94
95
  error do
@@ -125,7 +126,7 @@ module ActiveJob
125
126
  "Stopped retrying #{job.class} (Job ID: #{job.job_id}) due to a #{ex.class} (#{ex.message}), which reoccurred on #{job.executions} attempts."
126
127
  end
127
128
  end
128
- subscribe_log_level :enqueue_retry, :error
129
+ subscribe_log_level :retry_stopped, :error
129
130
 
130
131
  def discard(event)
131
132
  job = event.payload[:job]
@@ -137,6 +138,64 @@ module ActiveJob
137
138
  end
138
139
  subscribe_log_level :discard, :error
139
140
 
141
+ def interrupt(event)
142
+ job = event.payload[:job]
143
+ info do
144
+ "Interrupted #{job.class} (Job ID: #{job.job_id}) #{event.payload[:description]} (#{event.payload[:reason]})"
145
+ end
146
+ end
147
+ subscribe_log_level :interrupt, :info
148
+
149
+ def resume(event)
150
+ job = event.payload[:job]
151
+ info do
152
+ "Resuming #{job.class} (Job ID: #{job.job_id}) #{event.payload[:description]}"
153
+ end
154
+ end
155
+ subscribe_log_level :resume, :info
156
+
157
+ def step_skipped(event)
158
+ job = event.payload[:job]
159
+ info do
160
+ "Step '#{event.payload[:step].name}' skipped #{job.class}"
161
+ end
162
+ end
163
+ subscribe_log_level :step_skipped, :info
164
+
165
+ def step_started(event)
166
+ job = event.payload[:job]
167
+ step = event.payload[:step]
168
+ info do
169
+ if step.resumed?
170
+ "Step '#{step.name}' resumed from cursor '#{step.cursor}' for #{job.class} (Job ID: #{job.job_id})"
171
+ else
172
+ "Step '#{step.name}' started for #{job.class} (Job ID: #{job.job_id})"
173
+ end
174
+ end
175
+ end
176
+ subscribe_log_level :step_started, :info
177
+
178
+ def step(event)
179
+ job = event.payload[:job]
180
+ step = event.payload[:step]
181
+ ex = event.payload[:exception_object]
182
+
183
+ if event.payload[:interrupted]
184
+ info do
185
+ "Step '#{step.name}' interrupted at cursor '#{step.cursor}' for #{job.class} (Job ID: #{job.job_id}) in #{event.duration.round(2)}ms"
186
+ end
187
+ elsif ex
188
+ error do
189
+ "Error during step '#{step.name}' at cursor '#{step.cursor}' for #{job.class} (Job ID: #{job.job_id}) in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message})"
190
+ end
191
+ else
192
+ info do
193
+ "Step '#{step.name}' completed for #{job.class} (Job ID: #{job.job_id}) in #{event.duration.round(2)}ms"
194
+ end
195
+ end
196
+ end
197
+ subscribe_log_level :step, :error
198
+
140
199
  private
141
200
  def queue_name(event)
142
201
  ActiveJob.adapter_name(event.payload[:adapter]) + "(#{event.payload[:job].queue_name})"
@@ -189,15 +248,15 @@ module ActiveJob
189
248
  end
190
249
 
191
250
  def log_enqueue_source
192
- source = extract_enqueue_source_location(caller)
251
+ source = enqueue_source_location
193
252
 
194
253
  if source
195
254
  logger.info("↳ #{source}")
196
255
  end
197
256
  end
198
257
 
199
- def extract_enqueue_source_location(locations)
200
- backtrace_cleaner.clean(locations.lazy).first
258
+ def enqueue_source_location
259
+ backtrace_cleaner.first_clean_frame
201
260
  end
202
261
 
203
262
  def enqueued_jobs_message(adapter, enqueued_jobs)
@@ -4,17 +4,31 @@ require "active_support/tagged_logging"
4
4
  require "active_support/logger"
5
5
 
6
6
  module ActiveJob
7
- module Logging # :nodoc:
7
+ module Logging
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
+ ##
12
+ # Accepts a logger conforming to the interface of Log4r or the default
13
+ # Ruby +Logger+ class. You can retrieve this logger by calling +logger+ on
14
+ # either an Active Job job class or an Active Job job instance.
11
15
  cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
16
+
17
+ ##
18
+ # Configures whether a job's arguments should be logged. This can be
19
+ # useful when a job's arguments may be sensitive and so should not be
20
+ # logged.
21
+ #
22
+ # The value defaults to +true+, but this can be configured with
23
+ # +config.active_job.log_arguments+. Additionally, individual jobs can
24
+ # also configure a value, which will apply to themselves and any
25
+ # subclasses.
12
26
  class_attribute :log_arguments, instance_accessor: false, default: true
13
27
 
14
28
  around_enqueue(prepend: true) { |_, block| tag_logger(&block) }
15
29
  end
16
30
 
17
- def perform_now
31
+ def perform_now # :nodoc:
18
32
  tag_logger(self.class.name, self.job_id) { super }
19
33
  end
20
34
 
@@ -15,7 +15,8 @@ module ActiveJob
15
15
  # = Active Job Queue adapter
16
16
  #
17
17
  # The +ActiveJob::QueueAdapter+ module is used to load the
18
- # correct adapter. The default queue adapter is the +:async+ queue.
18
+ # correct adapter. The default queue adapter is +:async+,
19
+ # which loads the ActiveJob::QueueAdapters::AsyncAdapter.
19
20
  module QueueAdapter # :nodoc:
20
21
  extend ActiveSupport::Concern
21
22
 
@@ -24,21 +25,21 @@ module ActiveJob
24
25
  class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false
25
26
 
26
27
  delegate :queue_adapter, to: :class
27
-
28
- self.queue_adapter = :async
29
28
  end
30
29
 
31
30
  # Includes the setter method for changing the active queue adapter.
32
31
  module ClassMethods
33
32
  # Returns the backend queue provider. The default queue adapter
34
- # is the +:async+ queue. See QueueAdapters for more information.
33
+ # is +:async+. See QueueAdapters for more information.
35
34
  def queue_adapter
35
+ self.queue_adapter = :async if _queue_adapter.nil?
36
36
  _queue_adapter
37
37
  end
38
38
 
39
39
  # Returns string denoting the name of the configured queue adapter.
40
40
  # By default returns <tt>"async"</tt>.
41
41
  def queue_adapter_name
42
+ self.queue_adapter = :async if _queue_adapter_name.nil?
42
43
  _queue_adapter_name
43
44
  end
44
45
 
@@ -49,6 +50,7 @@ module ActiveJob
49
50
  case name_or_adapter
50
51
  when Symbol, String
51
52
  queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new
53
+ queue_adapter.try(:check_adapter)
52
54
  assign_adapter(name_or_adapter.to_s, queue_adapter)
53
55
  else
54
56
  if queue_adapter?(name_or_adapter)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module QueueAdapters
5
+ # = Active Job Abstract Adapter
6
+ #
7
+ # Active Job supports multiple job queue systems. ActiveJob::QueueAdapters::AbstractAdapter
8
+ # forms the abstraction layer which makes this possible.
9
+ class AbstractAdapter
10
+ attr_accessor :stopping
11
+
12
+ def enqueue(job)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def enqueue_at(job, timestamp)
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def stopping?
21
+ !!@stopping
22
+ end
23
+ end
24
+ end
25
+ end
@@ -30,7 +30,7 @@ module ActiveJob
30
30
  # The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
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
- class AsyncAdapter
33
+ class AsyncAdapter < AbstractAdapter
34
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)
@@ -74,7 +74,7 @@ module ActiveJob
74
74
  class Scheduler # :nodoc:
75
75
  DEFAULT_EXECUTOR_OPTIONS = {
76
76
  min_threads: 0,
77
- max_threads: Concurrent.processor_count,
77
+ max_threads: ENV.fetch("RAILS_MAX_THREADS", 5).to_i,
78
78
  auto_terminate: true,
79
79
  idletime: 60, # 1 minute
80
80
  max_queue: 0, # unlimited
@@ -86,7 +86,11 @@ module ActiveJob
86
86
  def initialize(**options)
87
87
  self.immediate = false
88
88
  @immediate_executor = Concurrent::ImmediateExecutor.new
89
- @async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options))
89
+ @async_executor = Concurrent::ThreadPoolExecutor.new(
90
+ name: "ActiveJob-async-scheduler",
91
+ **DEFAULT_EXECUTOR_OPTIONS,
92
+ **options
93
+ )
90
94
  end
91
95
 
92
96
  def enqueue(job, queue_name:)
@@ -14,7 +14,7 @@ module ActiveJob
14
14
  # To use Backburner set the queue_adapter config to +:backburner+.
15
15
  #
16
16
  # Rails.application.config.active_job.queue_adapter = :backburner
17
- class BackburnerAdapter
17
+ class BackburnerAdapter < AbstractAdapter
18
18
  def enqueue(job) # :nodoc:
19
19
  response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
20
20
  job.provider_job_id = response[:id] if response.is_a?(Hash)