activejob 7.2.3 → 8.1.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -62
  3. data/README.md +7 -5
  4. data/lib/active_job/arguments.rb +51 -48
  5. data/lib/active_job/base.rb +3 -4
  6. data/lib/active_job/configured_job.rb +6 -1
  7. data/lib/active_job/continuable.rb +102 -0
  8. data/lib/active_job/continuation/step.rb +83 -0
  9. data/lib/active_job/continuation/test_helper.rb +89 -0
  10. data/lib/active_job/continuation/validation.rb +50 -0
  11. data/lib/active_job/continuation.rb +332 -0
  12. data/lib/active_job/core.rb +21 -3
  13. data/lib/active_job/enqueue_after_transaction_commit.rb +20 -10
  14. data/lib/active_job/enqueuing.rb +11 -8
  15. data/lib/active_job/exceptions.rb +17 -8
  16. data/lib/active_job/execution_state.rb +11 -0
  17. data/lib/active_job/gem_version.rb +2 -2
  18. data/lib/active_job/instrumentation.rb +12 -12
  19. data/lib/active_job/log_subscriber.rb +63 -4
  20. data/lib/active_job/queue_adapter.rb +1 -0
  21. data/lib/active_job/queue_adapters/abstract_adapter.rb +5 -7
  22. data/lib/active_job/queue_adapters/async_adapter.rb +6 -2
  23. data/lib/active_job/queue_adapters/delayed_job_adapter.rb +0 -8
  24. data/lib/active_job/queue_adapters/inline_adapter.rb +0 -4
  25. data/lib/active_job/queue_adapters/queue_classic_adapter.rb +0 -8
  26. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +19 -0
  27. data/lib/active_job/queue_adapters/test_adapter.rb +5 -9
  28. data/lib/active_job/queue_adapters.rb +0 -4
  29. data/lib/active_job/railtie.rb +15 -6
  30. data/lib/active_job/serializers/action_controller_parameters_serializer.rb +25 -0
  31. data/lib/active_job/serializers/big_decimal_serializer.rb +3 -4
  32. data/lib/active_job/serializers/date_serializer.rb +3 -4
  33. data/lib/active_job/serializers/date_time_serializer.rb +3 -4
  34. data/lib/active_job/serializers/duration_serializer.rb +5 -6
  35. data/lib/active_job/serializers/module_serializer.rb +3 -4
  36. data/lib/active_job/serializers/object_serializer.rb +11 -14
  37. data/lib/active_job/serializers/range_serializer.rb +9 -9
  38. data/lib/active_job/serializers/symbol_serializer.rb +4 -5
  39. data/lib/active_job/serializers/time_serializer.rb +3 -4
  40. data/lib/active_job/serializers/time_with_zone_serializer.rb +3 -4
  41. data/lib/active_job/serializers.rb +62 -18
  42. data/lib/active_job/structured_event_subscriber.rb +220 -0
  43. data/lib/active_job.rb +3 -12
  44. metadata +16 -11
  45. data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -49
  46. data/lib/active_job/timezones.rb +0 -13
  47. data/lib/active_job/translation.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f76db92910f325154aa9d955728c51e2707232f2db258198ef3128d3f07ad26
4
- data.tar.gz: 3c68ce1d7ff44d94a9c877923e8f82de2f747d1a889e57c2bed0bd4fc71a41d5
3
+ metadata.gz: 3c0d6b95a55128dff7ad86a6fdb98667b180b715b4ea377a04eb7d23b2d3a7ad
4
+ data.tar.gz: 150e59464bdf3e5e67614ec986264ba846868d86030b996496b4ad6761f989e1
5
5
  SHA512:
6
- metadata.gz: 8af4d3675c132bf94582a852445b7aca33b7fdd44bea30c17e6ac7823c12c5d8857e712dc7f487ce1f5ab2fec1445bd94b0556aad19af66ccf6c5190c7c9f3e8
7
- data.tar.gz: 3560f91b6101294422260c4dfa4106d6588fccd7aa7b8ef94eaa3fbbf17b5380001ed6667d0619a68a6258612193c1677dd5e7e3a1613f9d1ff21368562687e8
6
+ metadata.gz: 2b287b9ed3ff07aeae8c8535ea29b650a0d76c477f44a3301ab4eff6016ec40da85dcb496f6d2b51b3fb903a6b6ed0652cd883e2504e937dccb3eba1fac5edeb
7
+ data.tar.gz: 507eb9a963bf908ce24883776f892a23b205d39530691a11f29b90d375ad464fcde5102808a6c38b75745a2180f8dda80298c479131395bfd1a98b7bbe529cf6
data/CHANGELOG.md CHANGED
@@ -1,111 +1,142 @@
1
- ## Rails 7.2.3 (October 28, 2025) ##
1
+ ## Rails 8.1.3 (March 24, 2026) ##
2
2
 
3
- * Include the actual Active Job locale when serializing rather than I18n locale.
4
-
5
- *Adrien S*
6
-
7
- * Avoid crashing in Active Job logger when logging enqueueing errors
8
-
9
- `ActiveJob.perform_all_later` could fail with a `TypeError` when all
10
- provided jobs failed to be enqueueed.
11
-
12
- *Efstathios Stivaros*
3
+ * No changes.
13
4
 
14
5
 
15
- ## Rails 7.2.2.2 (August 13, 2025) ##
6
+ ## Rails 8.1.2.1 (March 23, 2026) ##
16
7
 
17
8
  * No changes.
18
9
 
19
10
 
20
- ## Rails 7.2.2.1 (December 10, 2024) ##
11
+ ## Rails 8.1.2 (January 08, 2026) ##
21
12
 
22
- * No changes.
13
+ * Fix `ActiveJob.perform_all_later` to respect `job_class.enqueue_after_transaction_commit`.
23
14
 
15
+ Previously, `perform_all_later` would enqueue all jobs immediately, even if
16
+ they had `enqueue_after_transaction_commit = true`. Now it correctly defers
17
+ jobs with this setting until after transaction commits, matching the behavior
18
+ of `perform_later`.
24
19
 
25
- ## Rails 7.2.2 (October 30, 2024) ##
20
+ *OuYangJinTing*
26
21
 
27
- * No changes.
22
+ * Fix using custom serializers with `ActiveJob::Arguments.serialize` when
23
+ `ActiveJob::Base` hasn't been loaded.
28
24
 
25
+ *Hartley McGuire*
29
26
 
30
- ## Rails 7.2.1.2 (October 23, 2024) ##
27
+ ## Rails 8.1.1 (October 28, 2025) ##
31
28
 
32
- * No changes.
29
+ * Only index new serializers.
33
30
 
31
+ *Jesse Sharps*
34
32
 
35
- ## Rails 7.2.1.1 (October 15, 2024) ##
36
33
 
37
- * No changes.
34
+ ## Rails 8.1.0 (October 22, 2025) ##
38
35
 
36
+ * Add structured events for Active Job:
37
+ - `active_job.enqueued`
38
+ - `active_job.bulk_enqueued`
39
+ - `active_job.started`
40
+ - `active_job.completed`
41
+ - `active_job.retry_scheduled`
42
+ - `active_job.retry_stopped`
43
+ - `active_job.discarded`
44
+ - `active_job.interrupt`
45
+ - `active_job.resume`
46
+ - `active_job.step_skipped`
47
+ - `active_job.step_started`
48
+ - `active_job.step`
39
49
 
40
- ## Rails 7.2.1 (August 22, 2024) ##
50
+ *Adrianna Chang*
41
51
 
42
- * No changes.
52
+ * Deprecate built-in `sidekiq` adapter.
53
+
54
+ If you're using this adapter, upgrade to `sidekiq` 7.3.3 or later to use the `sidekiq` gem's adapter.
43
55
 
56
+ *fatkodima*
44
57
 
45
- ## Rails 7.2.0 (August 09, 2024) ##
58
+ * Remove deprecated internal `SuckerPunch` adapter in favor of the adapter included with the `sucker_punch` gem.
46
59
 
47
- * All tests now respect the `active_job.queue_adapter` config.
60
+ *Rafael Mendonça França*
48
61
 
49
- Previously if you had set `config.active_job.queue_adapter` in your `config/application.rb`
50
- or `config/environments/test.rb` file, the adapter you selected was previously not used consistently
51
- across all tests. In some tests your adapter would be used, but other tests would use the `TestAdapter`.
62
+ * Remove support to set `ActiveJob::Base.enqueue_after_transaction_commit` to `:never`, `:always` and `:default`.
52
63
 
53
- In Rails 7.2, all tests will respect the `queue_adapter` config if provided. If no config is provided,
54
- the `TestAdapter` will continue to be used.
64
+ *Rafael Mendonça França*
55
65
 
56
- See [#48585](https://github.com/rails/rails/pull/48585) for more details.
66
+ * Remove deprecated `Rails.application.config.active_job.enqueue_after_transaction_commit`.
57
67
 
58
- *Alex Ghiculescu*
68
+ *Rafael Mendonça França*
59
69
 
60
- * Make Active Job transaction aware when used conjointly with Active Record.
70
+ * `ActiveJob::Serializers::ObjectSerializers#klass` method is now public.
61
71
 
62
- A common mistake with Active Job is to enqueue jobs from inside a transaction,
63
- causing them to potentially be picked and ran by another process, before the
64
- transaction is committed, which may result in various errors.
72
+ Custom Active Job serializers must have a public `#klass` method too.
73
+ The returned class will be index allowing for faster serialization.
65
74
 
66
- ```ruby
67
- Topic.transaction do
68
- topic = Topic.create(...)
69
- NewTopicNotificationJob.perform_later(topic)
70
- end
71
- ```
75
+ *Jean Boussier*
72
76
 
73
- Now Active Job will automatically defer the enqueuing to after the transaction is committed,
74
- and drop the job if the transaction is rolled back.
77
+ * Allow jobs to the interrupted and resumed with Continuations
75
78
 
76
- Various queue implementations can choose to disable this behavior, and users can disable it,
77
- or force it on a per job basis:
79
+ A job can use Continuations by including the `ActiveJob::Continuable`
80
+ concern. Continuations split jobs into steps. When the queuing system
81
+ is shutting down jobs can be interrupted and their progress saved.
78
82
 
79
83
  ```ruby
80
- class NewTopicNotificationJob < ApplicationJob
81
- self.enqueue_after_transaction_commit = :never # or `:always` or `:default`
84
+ class ProcessImportJob
85
+ include ActiveJob::Continuable
86
+
87
+ def perform(import_id)
88
+ @import = Import.find(import_id)
89
+
90
+ # block format
91
+ step :initialize do
92
+ @import.initialize
93
+ end
94
+
95
+ # step with cursor, the cursor is saved when the job is interrupted
96
+ step :process do |step|
97
+ @import.records.find_each(start: step.cursor) do |record|
98
+ record.process
99
+ step.advance! from: record.id
100
+ end
101
+ end
102
+
103
+ # method format
104
+ step :finalize
105
+
106
+ private
107
+ def finalize
108
+ @import.finalize
109
+ end
110
+ end
82
111
  end
83
112
  ```
84
113
 
85
- *Jean Boussier*, *Cristian Bica*
114
+ *Donal McBreen*
86
115
 
87
- * Do not trigger immediate loading of `ActiveJob::Base` when loading `ActiveJob::TestHelper`.
116
+ * Defer invocation of ActiveJob enqueue callbacks until after commit when
117
+ `enqueue_after_transaction_commit` is enabled.
88
118
 
89
- *Maxime Réty*
119
+ *Will Roever*
90
120
 
91
- * Preserve the serialized timezone when deserializing `ActiveSupport::TimeWithZone` arguments.
121
+ * Add `report:` option to `ActiveJob::Base#retry_on` and `#discard_on`
92
122
 
93
- *Joshua Young*
123
+ When the `report:` option is passed, errors will be reported to the error reporter
124
+ before being retried / discarded.
94
125
 
95
- * Remove deprecated `:exponentially_longer` value for the `:wait` in `retry_on`.
126
+ *Andrew Novoselac*
96
127
 
97
- *Rafael Mendonça França*
128
+ * Accept a block for `ActiveJob::ConfiguredJob#perform_later`.
98
129
 
99
- * Remove deprecated support to set numeric values to `scheduled_at` attribute.
130
+ This was inconsistent with a regular `ActiveJob::Base#perform_later`.
100
131
 
101
- *Rafael Mendonça França*
132
+ *fatkodima*
102
133
 
103
- * Deprecate `Rails.application.config.active_job.use_big_decimal_serializer`.
134
+ * Raise a more specific error during deserialization when a previously serialized job class is now unknown.
104
135
 
105
- *Rafael Mendonça França*
136
+ `ActiveJob::UnknownJobClassError` will be raised instead of a more generic
137
+ `NameError` to make it easily possible for adapters to tell if the `NameError`
138
+ was raised during job execution or deserialization.
106
139
 
107
- * Remove deprecated primitive serializer for `BigDecimal` arguments.
108
-
109
- *Rafael Mendonça França*
140
+ *Earlopain*
110
141
 
111
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activejob/CHANGELOG.md) for previous changes.
142
+ Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activejob/CHANGELOG.md) for previous changes.
data/README.md CHANGED
@@ -89,11 +89,13 @@ Active Job has built-in adapters for multiple queuing backends (Sidekiq,
89
89
  Resque, Delayed Job and others). To get an up-to-date list of the adapters
90
90
  see the API Documentation for [ActiveJob::QueueAdapters](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
91
91
 
92
- **Please note:** We are not accepting pull requests for new adapters. We
93
- encourage library authors to provide an ActiveJob adapter as part of
94
- their gem, or as a stand-alone gem. For discussion about this see the
95
- following PRs: [23311](https://github.com/rails/rails/issues/23311#issuecomment-176275718),
96
- [21406](https://github.com/rails/rails/pull/21406#issuecomment-138813484), and [#32285](https://github.com/rails/rails/pull/32285).
92
+ **Please note:** We are not accepting pull requests for new adapters, and we are
93
+ actively extracting the current adapters. We encourage library authors to provide
94
+ an Active Job adapter as part of their gem, or as a stand-alone gem.
95
+
96
+ ## Continuations
97
+
98
+ Continuations allow jobs to be interrupted and resumed. See more at ActiveJob::Continuation.
97
99
 
98
100
 
99
101
  ## Download and installation
@@ -35,6 +35,46 @@ module ActiveJob
35
35
  arguments.map { |argument| serialize_argument(argument) }
36
36
  end
37
37
 
38
+ def serialize_argument(argument) # :nodoc:
39
+ case argument
40
+ when nil, true, false, Integer, Float # Types that can hardly be subclassed
41
+ argument
42
+ when String
43
+ if argument.class == String
44
+ argument
45
+ else
46
+ begin
47
+ Serializers.serialize(argument)
48
+ rescue SerializationError
49
+ argument
50
+ end
51
+ end
52
+ when Symbol
53
+ { Serializers::OBJECT_SERIALIZER_KEY => "ActiveJob::Serializers::SymbolSerializer", "value" => argument.name }
54
+ when GlobalID::Identification
55
+ convert_to_global_id_hash(argument)
56
+ when Array
57
+ argument.map { |arg| serialize_argument(arg) }
58
+ when ActiveSupport::HashWithIndifferentAccess
59
+ serialize_indifferent_hash(argument)
60
+ when Hash
61
+ symbol_keys = argument.keys
62
+ symbol_keys.select! { |k| k.is_a?(Symbol) }
63
+ symbol_keys.map!(&:name)
64
+
65
+ aj_hash_key = if Hash.ruby2_keywords_hash?(argument)
66
+ RUBY2_KEYWORDS_KEY
67
+ else
68
+ SYMBOL_KEYS_KEY
69
+ end
70
+ result = serialize_hash(argument)
71
+ result[aj_hash_key] = symbol_keys
72
+ result
73
+ else
74
+ Serializers.serialize(argument)
75
+ end
76
+ end
77
+
38
78
  # Deserializes a set of arguments. Intrinsic types that can safely be
39
79
  # deserialized without mutation are returned as-is. Arrays/Hashes are
40
80
  # deserialized element by element. All other types are deserialized using
@@ -54,59 +94,18 @@ module ActiveJob
54
94
  RUBY2_KEYWORDS_KEY = "_aj_ruby2_keywords"
55
95
  # :nodoc:
56
96
  WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access"
57
- # :nodoc:
58
- OBJECT_SERIALIZER_KEY = "_aj_serialized"
59
97
 
60
98
  # :nodoc:
61
99
  RESERVED_KEYS = [
62
100
  GLOBALID_KEY, GLOBALID_KEY.to_sym,
63
101
  SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
64
102
  RUBY2_KEYWORDS_KEY, RUBY2_KEYWORDS_KEY.to_sym,
65
- OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym,
103
+ Serializers::OBJECT_SERIALIZER_KEY, Serializers::OBJECT_SERIALIZER_KEY.to_sym,
66
104
  WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
67
- ]
105
+ ].to_set
68
106
  private_constant :RESERVED_KEYS, :GLOBALID_KEY,
69
107
  :SYMBOL_KEYS_KEY, :RUBY2_KEYWORDS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
70
108
 
71
- def serialize_argument(argument)
72
- case argument
73
- when nil, true, false, Integer, Float # Types that can hardly be subclassed
74
- argument
75
- when String
76
- if argument.class == String
77
- argument
78
- else
79
- begin
80
- Serializers.serialize(argument)
81
- rescue SerializationError
82
- argument
83
- end
84
- end
85
- when GlobalID::Identification
86
- convert_to_global_id_hash(argument)
87
- when Array
88
- argument.map { |arg| serialize_argument(arg) }
89
- when ActiveSupport::HashWithIndifferentAccess
90
- serialize_indifferent_hash(argument)
91
- when Hash
92
- symbol_keys = argument.each_key.grep(Symbol).map!(&:to_s)
93
- aj_hash_key = if Hash.ruby2_keywords_hash?(argument)
94
- RUBY2_KEYWORDS_KEY
95
- else
96
- SYMBOL_KEYS_KEY
97
- end
98
- result = serialize_hash(argument)
99
- result[aj_hash_key] = symbol_keys
100
- result
101
- else
102
- if argument.respond_to?(:permitted?) && argument.respond_to?(:to_h)
103
- serialize_indifferent_hash(argument.to_h)
104
- else
105
- Serializers.serialize(argument)
106
- end
107
- end
108
- end
109
-
110
109
  def deserialize_argument(argument)
111
110
  case argument
112
111
  when nil, true, false, String, Integer, Float
@@ -135,7 +134,7 @@ module ActiveJob
135
134
  end
136
135
 
137
136
  def custom_serialized?(hash)
138
- hash.key?(OBJECT_SERIALIZER_KEY)
137
+ hash.key?(Serializers::OBJECT_SERIALIZER_KEY)
139
138
  end
140
139
 
141
140
  def serialize_hash(argument)
@@ -159,10 +158,12 @@ module ActiveJob
159
158
 
160
159
  def serialize_hash_key(key)
161
160
  case key
162
- when *RESERVED_KEYS
161
+ when RESERVED_KEYS
163
162
  raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}")
164
- when String, Symbol
165
- key.to_s
163
+ when String
164
+ key
165
+ when Symbol
166
+ key.name
166
167
  else
167
168
  raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}")
168
169
  end
@@ -170,7 +171,7 @@ module ActiveJob
170
171
 
171
172
  def serialize_indifferent_hash(indifferent_hash)
172
173
  result = serialize_hash(indifferent_hash)
173
- result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
174
+ result[WITH_INDIFFERENT_ACCESS_KEY] = true
174
175
  result
175
176
  end
176
177
 
@@ -194,4 +195,6 @@ module ActiveJob
194
195
  "without an id. (Maybe you forgot to call save?)"
195
196
  end
196
197
  end
198
+
199
+ ActiveSupport.run_load_hooks(:active_job_arguments, Arguments)
197
200
  end
@@ -9,10 +9,10 @@ require "active_job/execution"
9
9
  require "active_job/callbacks"
10
10
  require "active_job/exceptions"
11
11
  require "active_job/log_subscriber"
12
+ require "active_job/structured_event_subscriber"
12
13
  require "active_job/logging"
13
14
  require "active_job/instrumentation"
14
- require "active_job/timezones"
15
- require "active_job/translation"
15
+ require "active_job/execution_state"
16
16
 
17
17
  module ActiveJob # :nodoc:
18
18
  # = Active Job \Base
@@ -71,8 +71,7 @@ module ActiveJob # :nodoc:
71
71
  include Exceptions
72
72
  include Instrumentation
73
73
  include Logging
74
- include Timezones
75
- include Translation
74
+ include ExecutionState
76
75
 
77
76
  ActiveSupport.run_load_hooks(:active_job, self)
78
77
  end
@@ -12,7 +12,12 @@ module ActiveJob
12
12
  end
13
13
 
14
14
  def perform_later(...)
15
- @job_class.new(...).enqueue @options
15
+ job = @job_class.new(...)
16
+ enqueue_result = job.enqueue(@options)
17
+
18
+ yield job if block_given?
19
+
20
+ enqueue_result
16
21
  end
17
22
  end
18
23
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ # = Active Job Continuable
5
+ #
6
+ # The Continuable module provides the ability to track the progress of your
7
+ # jobs, and continue from where they left off if interrupted.
8
+ #
9
+ # Mix ActiveJob::Continuable into your job to enable continuations.
10
+ #
11
+ # See {ActiveJob::Continuation}[rdoc-ref:ActiveJob::Continuation] for usage.
12
+ #
13
+ module Continuable
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ class_attribute :max_resumptions, instance_writer: false
18
+ class_attribute :resume_options, instance_writer: false, default: { wait: 5.seconds }
19
+ class_attribute :resume_errors_after_advancing, instance_writer: false, default: true
20
+
21
+ around_perform :continue
22
+
23
+ def initialize(...)
24
+ super(...)
25
+ self.resumptions = 0
26
+ self.continuation = Continuation.new(self, {})
27
+ end
28
+ end
29
+
30
+ # The number of times the job has been resumed.
31
+ attr_accessor :resumptions
32
+
33
+ attr_accessor :continuation # :nodoc:
34
+
35
+ # Start a new continuation step
36
+ def step(step_name, start: nil, isolated: false, &block)
37
+ unless block_given?
38
+ step_method = method(step_name)
39
+
40
+ raise ArgumentError, "Step method '#{step_name}' must accept 0 or 1 arguments" if step_method.arity > 1
41
+
42
+ if step_method.parameters.any? { |type, name| type == :key || type == :keyreq }
43
+ raise ArgumentError, "Step method '#{step_name}' must not accept keyword arguments"
44
+ end
45
+
46
+ block = step_method.arity == 0 ? -> (_) { step_method.call } : step_method
47
+ end
48
+ checkpoint! if continuation.advanced?
49
+ continuation.step(step_name, start: start, isolated: isolated, &block)
50
+ end
51
+
52
+ def serialize # :nodoc:
53
+ super.merge("continuation" => continuation.to_h, "resumptions" => resumptions)
54
+ end
55
+
56
+ def deserialize(job_data) # :nodoc:
57
+ super
58
+ self.continuation = Continuation.new(self, job_data.fetch("continuation", {}))
59
+ self.resumptions = job_data.fetch("resumptions", 0)
60
+ end
61
+
62
+ def checkpoint! # :nodoc:
63
+ interrupt!(reason: :stopping) if queue_adapter.stopping?
64
+ end
65
+
66
+ def interrupt!(reason:) # :nodoc:
67
+ instrument :interrupt, reason: reason, **continuation.instrumentation
68
+ raise Continuation::Interrupt, "Interrupted #{continuation.description} (#{reason})"
69
+ end
70
+
71
+ private
72
+ def continue(&block)
73
+ if continuation.started?
74
+ self.resumptions += 1
75
+ instrument :resume, **continuation.instrumentation
76
+ end
77
+
78
+ block.call
79
+ rescue Continuation::Interrupt => e
80
+ resume_job(e)
81
+ rescue Continuation::Error
82
+ raise
83
+ rescue StandardError => e
84
+ if resume_errors_after_advancing? && continuation.advanced?
85
+ resume_job(exception: e)
86
+ else
87
+ raise
88
+ end
89
+ end
90
+
91
+ def resume_job(exception) # :nodoc:
92
+ executions_for(exception)
93
+ if max_resumptions.nil? || resumptions < max_resumptions
94
+ retry_job(**self.resume_options)
95
+ else
96
+ raise Continuation::ResumeLimitError, "Job was resumed a maximum of #{max_resumptions} times"
97
+ end
98
+ end
99
+ end
100
+
101
+ ActiveSupport.run_load_hooks(:active_job_continuable, Continuable)
102
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ class Continuation
5
+ # = Active Job Continuation Step
6
+ #
7
+ # Represents a step within a continuable job.
8
+ #
9
+ # When a step is completed, it is recorded in the job's continuation state.
10
+ # If the job is interrupted, it will be resumed from after the last completed step.
11
+ #
12
+ # Steps also have an optional cursor that can be used to track progress within the step.
13
+ # If a job is interrupted during a step, the cursor will be saved and passed back when
14
+ # the job is resumed.
15
+ #
16
+ # It is the responsibility of the code in the step to use the cursor correctly to resume
17
+ # from where it left off.
18
+ class Step
19
+ # The name of the step.
20
+ attr_reader :name
21
+
22
+ # The cursor for the step.
23
+ attr_reader :cursor
24
+
25
+ def initialize(name, cursor, job:, resumed:)
26
+ @name = name.to_sym
27
+ @initial_cursor = cursor
28
+ @cursor = cursor
29
+ @resumed = resumed
30
+ @job = job
31
+ end
32
+
33
+ # Check if the job should be interrupted, and if so raise an Interrupt exception.
34
+ # The job will be requeued for retry.
35
+ def checkpoint!
36
+ job.checkpoint!
37
+ end
38
+
39
+ # Set the cursor and interrupt the job if necessary.
40
+ def set!(cursor)
41
+ @cursor = cursor
42
+ checkpoint!
43
+ end
44
+
45
+ # Advance the cursor from the current or supplied value
46
+ #
47
+ # The cursor will be advanced by calling the +succ+ method on the cursor.
48
+ # An UnadvanceableCursorError error will be raised if the cursor does not implement +succ+.
49
+ def advance!(from: nil)
50
+ from = cursor if from.nil?
51
+
52
+ begin
53
+ to = from.succ
54
+ rescue NoMethodError
55
+ raise UnadvanceableCursorError, "Cursor class '#{from.class}' does not implement 'succ'"
56
+ end
57
+
58
+ set! to
59
+ end
60
+
61
+ # Has this step been resumed from a previous job execution?
62
+ def resumed?
63
+ @resumed
64
+ end
65
+
66
+ # Has the cursor been advanced during this job execution?
67
+ def advanced?
68
+ initial_cursor != cursor
69
+ end
70
+
71
+ def to_a
72
+ [ name.to_s, cursor ]
73
+ end
74
+
75
+ def description
76
+ "at '#{name}', cursor '#{cursor.inspect}'"
77
+ end
78
+
79
+ private
80
+ attr_reader :initial_cursor, :job
81
+ end
82
+ end
83
+ end