activejob 7.2.2.1 → 8.1.2

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 +90 -50
  3. data/README.md +8 -6
  4. data/lib/active_job/arguments.rb +51 -48
  5. data/lib/active_job/base.rb +5 -6
  6. data/lib/active_job/configured_job.rb +5 -4
  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 +22 -4
  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 +16 -6
  16. data/lib/active_job/execution_state.rb +11 -0
  17. data/lib/active_job/gem_version.rb +3 -3
  18. data/lib/active_job/instrumentation.rb +12 -12
  19. data/lib/active_job/log_subscriber.rb +64 -5
  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 +17 -15
  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: c3b47492def92c2f9d51610a3b8ca696cbb32ff69fe45107ebaf478a69abc65f
4
- data.tar.gz: f05813374e5dec3e6b9e138585ef9ef23c87640187a42ea03e7db9d50d1e2362
3
+ metadata.gz: b3a578cc3f9f7edc22dc86fba167b809a884264b4dd3a1191a9134f7b2620f84
4
+ data.tar.gz: 3d2f8faf8490ccaeb1b12f150b7c2f863b071451020ed79bbb4ae4dcd4f635e8
5
5
  SHA512:
6
- metadata.gz: 58e96af8f140d46c83bc3d9231ee6e8c330d71e724def12d20844d5af4280b12a4de63ba3c7e58157d32295193c55405fb292071c6f0494416f66ea46ad658a6
7
- data.tar.gz: a5f662c33aa37b48aa7643299cddaf5f76559feb7820468862b71aae7c0f725cf25ad098b0df2d951c810d76abaa10e49e24bdac3589b4c6bd9b683335856424
6
+ metadata.gz: 56ab511d1418ff2d13a4fe6fb19336899006ebc0f07f56c1bcc2de83bb27425974804fc63d95db64988638d0401455f28e183b537af19a8ed741fdb05253c584
7
+ data.tar.gz: ce679bda3a7b4ba95a12c0555a0334d4db859ccd57821468d49c148d6231165d7c100b5d6ae951cb02c9812f2ccc0a521eb7547217a1b2aefd6a77394c6f79d4
data/CHANGELOG.md CHANGED
@@ -1,92 +1,132 @@
1
- ## Rails 7.2.2.1 (December 10, 2024) ##
1
+ ## Rails 8.1.2 (January 08, 2026) ##
2
2
 
3
- * No changes.
3
+ * Fix `ActiveJob.perform_all_later` to respect `job_class.enqueue_after_transaction_commit`.
4
4
 
5
+ Previously, `perform_all_later` would enqueue all jobs immediately, even if
6
+ they had `enqueue_after_transaction_commit = true`. Now it correctly defers
7
+ jobs with this setting until after transaction commits, matching the behavior
8
+ of `perform_later`.
5
9
 
6
- ## Rails 7.2.2 (October 30, 2024) ##
10
+ *OuYangJinTing*
7
11
 
8
- * No changes.
12
+ * Fix using custom serializers with `ActiveJob::Arguments.serialize` when
13
+ `ActiveJob::Base` hasn't been loaded.
9
14
 
15
+ *Hartley McGuire*
10
16
 
11
- ## Rails 7.2.1.2 (October 23, 2024) ##
17
+ ## Rails 8.1.1 (October 28, 2025) ##
12
18
 
13
- * No changes.
19
+ * Only index new serializers.
14
20
 
21
+ *Jesse Sharps*
15
22
 
16
- ## Rails 7.2.1.1 (October 15, 2024) ##
17
23
 
18
- * No changes.
24
+ ## Rails 8.1.0 (October 22, 2025) ##
19
25
 
26
+ * Add structured events for Active Job:
27
+ - `active_job.enqueued`
28
+ - `active_job.bulk_enqueued`
29
+ - `active_job.started`
30
+ - `active_job.completed`
31
+ - `active_job.retry_scheduled`
32
+ - `active_job.retry_stopped`
33
+ - `active_job.discarded`
34
+ - `active_job.interrupt`
35
+ - `active_job.resume`
36
+ - `active_job.step_skipped`
37
+ - `active_job.step_started`
38
+ - `active_job.step`
20
39
 
21
- ## Rails 7.2.1 (August 22, 2024) ##
40
+ *Adrianna Chang*
22
41
 
23
- * No changes.
42
+ * Deprecate built-in `sidekiq` adapter.
24
43
 
44
+ If you're using this adapter, upgrade to `sidekiq` 7.3.3 or later to use the `sidekiq` gem's adapter.
25
45
 
26
- ## Rails 7.2.0 (August 09, 2024) ##
46
+ *fatkodima*
27
47
 
28
- * All tests now respect the `active_job.queue_adapter` config.
48
+ * Remove deprecated internal `SuckerPunch` adapter in favor of the adapter included with the `sucker_punch` gem.
29
49
 
30
- Previously if you had set `config.active_job.queue_adapter` in your `config/application.rb`
31
- or `config/environments/test.rb` file, the adapter you selected was previously not used consistently
32
- across all tests. In some tests your adapter would be used, but other tests would use the `TestAdapter`.
50
+ *Rafael Mendonça França*
33
51
 
34
- In Rails 7.2, all tests will respect the `queue_adapter` config if provided. If no config is provided,
35
- the `TestAdapter` will continue to be used.
52
+ * Remove support to set `ActiveJob::Base.enqueue_after_transaction_commit` to `:never`, `:always` and `:default`.
36
53
 
37
- See [#48585](https://github.com/rails/rails/pull/48585) for more details.
54
+ *Rafael Mendonça França*
38
55
 
39
- *Alex Ghiculescu*
56
+ * Remove deprecated `Rails.application.config.active_job.enqueue_after_transaction_commit`.
40
57
 
41
- * Make Active Job transaction aware when used conjointly with Active Record.
58
+ *Rafael Mendonça França*
42
59
 
43
- A common mistake with Active Job is to enqueue jobs from inside a transaction,
44
- causing them to potentially be picked and ran by another process, before the
45
- transaction is committed, which may result in various errors.
60
+ * `ActiveJob::Serializers::ObjectSerializers#klass` method is now public.
46
61
 
47
- ```ruby
48
- Topic.transaction do
49
- topic = Topic.create(...)
50
- NewTopicNotificationJob.perform_later(topic)
51
- end
52
- ```
62
+ Custom Active Job serializers must have a public `#klass` method too.
63
+ The returned class will be index allowing for faster serialization.
53
64
 
54
- Now Active Job will automatically defer the enqueuing to after the transaction is committed,
55
- and drop the job if the transaction is rolled back.
65
+ *Jean Boussier*
56
66
 
57
- Various queue implementations can choose to disable this behavior, and users can disable it,
58
- or force it on a per job basis:
67
+ * Allow jobs to the interrupted and resumed with Continuations
68
+
69
+ A job can use Continuations by including the `ActiveJob::Continuable`
70
+ concern. Continuations split jobs into steps. When the queuing system
71
+ is shutting down jobs can be interrupted and their progress saved.
59
72
 
60
73
  ```ruby
61
- class NewTopicNotificationJob < ApplicationJob
62
- self.enqueue_after_transaction_commit = :never # or `:always` or `:default`
74
+ class ProcessImportJob
75
+ include ActiveJob::Continuable
76
+
77
+ def perform(import_id)
78
+ @import = Import.find(import_id)
79
+
80
+ # block format
81
+ step :initialize do
82
+ @import.initialize
83
+ end
84
+
85
+ # step with cursor, the cursor is saved when the job is interrupted
86
+ step :process do |step|
87
+ @import.records.find_each(start: step.cursor) do |record|
88
+ record.process
89
+ step.advance! from: record.id
90
+ end
91
+ end
92
+
93
+ # method format
94
+ step :finalize
95
+
96
+ private
97
+ def finalize
98
+ @import.finalize
99
+ end
100
+ end
63
101
  end
64
102
  ```
65
103
 
66
- *Jean Boussier*, *Cristian Bica*
67
-
68
- * Do not trigger immediate loading of `ActiveJob::Base` when loading `ActiveJob::TestHelper`.
104
+ *Donal McBreen*
69
105
 
70
- *Maxime Réty*
106
+ * Defer invocation of ActiveJob enqueue callbacks until after commit when
107
+ `enqueue_after_transaction_commit` is enabled.
71
108
 
72
- * Preserve the serialized timezone when deserializing `ActiveSupport::TimeWithZone` arguments.
109
+ *Will Roever*
73
110
 
74
- *Joshua Young*
111
+ * Add `report:` option to `ActiveJob::Base#retry_on` and `#discard_on`
75
112
 
76
- * Remove deprecated `:exponentially_longer` value for the `:wait` in `retry_on`.
113
+ When the `report:` option is passed, errors will be reported to the error reporter
114
+ before being retried / discarded.
77
115
 
78
- *Rafael Mendonça França*
116
+ *Andrew Novoselac*
79
117
 
80
- * Remove deprecated support to set numeric values to `scheduled_at` attribute.
118
+ * Accept a block for `ActiveJob::ConfiguredJob#perform_later`.
81
119
 
82
- *Rafael Mendonça França*
120
+ This was inconsistent with a regular `ActiveJob::Base#perform_later`.
83
121
 
84
- * Deprecate `Rails.application.config.active_job.use_big_decimal_serialize`.
122
+ *fatkodima*
85
123
 
86
- *Rafael Mendonça França*
124
+ * Raise a more specific error during deserialization when a previously serialized job class is now unknown.
87
125
 
88
- * Remove deprecated primitive serializer for `BigDecimal` arguments.
126
+ `ActiveJob::UnknownJobClassError` will be raised instead of a more generic
127
+ `NameError` to make it easily possible for adapters to tell if the `NameError`
128
+ was raised during job execution or deserialization.
89
129
 
90
- *Rafael Mendonça França*
130
+ *Earlopain*
91
131
 
92
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activejob/CHANGELOG.md) for previous changes.
132
+ 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
@@ -126,6 +128,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
126
128
 
127
129
  * https://github.com/rails/rails/issues
128
130
 
129
- Feature requests should be discussed on the rails-core mailing list here:
131
+ Feature requests should be discussed on the rubyonrails-core forum here:
130
132
 
131
133
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -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
@@ -40,7 +40,7 @@ module ActiveJob # :nodoc:
40
40
  # end
41
41
  #
42
42
  # Records that are passed in are serialized/deserialized using Global
43
- # ID. More information can be found in Arguments.
43
+ # ID. More information can be found in ActiveJob::Arguments.
44
44
  #
45
45
  # To enqueue a job to be performed as soon as the queuing system is free:
46
46
  #
@@ -50,7 +50,7 @@ module ActiveJob # :nodoc:
50
50
  #
51
51
  # ProcessPhotoJob.set(wait_until: Date.tomorrow.noon).perform_later(photo)
52
52
  #
53
- # More information can be found in ActiveJob::Core::ClassMethods#set
53
+ # More information can be found in ActiveJob::Core::ClassMethods#set.
54
54
  #
55
55
  # A job can also be processed immediately without sending to the queue:
56
56
  #
@@ -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,11 +12,12 @@ module ActiveJob
12
12
  end
13
13
 
14
14
  def perform_later(...)
15
- @job_class.new(...).enqueue @options
16
- end
15
+ job = @job_class.new(...)
16
+ enqueue_result = job.enqueue(@options)
17
+
18
+ yield job if block_given?
17
19
 
18
- def perform_all_later(multi_args)
19
- @job_class.perform_all_later(multi_args, options: @options)
20
+ enqueue_result
20
21
  end
21
22
  end
22
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