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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -62
- data/README.md +7 -5
- data/lib/active_job/arguments.rb +51 -48
- data/lib/active_job/base.rb +3 -4
- data/lib/active_job/configured_job.rb +6 -1
- data/lib/active_job/continuable.rb +102 -0
- data/lib/active_job/continuation/step.rb +83 -0
- data/lib/active_job/continuation/test_helper.rb +89 -0
- data/lib/active_job/continuation/validation.rb +50 -0
- data/lib/active_job/continuation.rb +332 -0
- data/lib/active_job/core.rb +21 -3
- data/lib/active_job/enqueue_after_transaction_commit.rb +20 -10
- data/lib/active_job/enqueuing.rb +11 -8
- data/lib/active_job/exceptions.rb +17 -8
- data/lib/active_job/execution_state.rb +11 -0
- data/lib/active_job/gem_version.rb +2 -2
- data/lib/active_job/instrumentation.rb +12 -12
- data/lib/active_job/log_subscriber.rb +63 -4
- data/lib/active_job/queue_adapter.rb +1 -0
- data/lib/active_job/queue_adapters/abstract_adapter.rb +5 -7
- data/lib/active_job/queue_adapters/async_adapter.rb +6 -2
- data/lib/active_job/queue_adapters/delayed_job_adapter.rb +0 -8
- data/lib/active_job/queue_adapters/inline_adapter.rb +0 -4
- data/lib/active_job/queue_adapters/queue_classic_adapter.rb +0 -8
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +19 -0
- data/lib/active_job/queue_adapters/test_adapter.rb +5 -9
- data/lib/active_job/queue_adapters.rb +0 -4
- data/lib/active_job/railtie.rb +15 -6
- data/lib/active_job/serializers/action_controller_parameters_serializer.rb +25 -0
- data/lib/active_job/serializers/big_decimal_serializer.rb +3 -4
- data/lib/active_job/serializers/date_serializer.rb +3 -4
- data/lib/active_job/serializers/date_time_serializer.rb +3 -4
- data/lib/active_job/serializers/duration_serializer.rb +5 -6
- data/lib/active_job/serializers/module_serializer.rb +3 -4
- data/lib/active_job/serializers/object_serializer.rb +11 -14
- data/lib/active_job/serializers/range_serializer.rb +9 -9
- data/lib/active_job/serializers/symbol_serializer.rb +4 -5
- data/lib/active_job/serializers/time_serializer.rb +3 -4
- data/lib/active_job/serializers/time_with_zone_serializer.rb +3 -4
- data/lib/active_job/serializers.rb +62 -18
- data/lib/active_job/structured_event_subscriber.rb +220 -0
- data/lib/active_job.rb +3 -12
- metadata +16 -11
- data/lib/active_job/queue_adapters/sucker_punch_adapter.rb +0 -49
- data/lib/active_job/timezones.rb +0 -13
- data/lib/active_job/translation.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c0d6b95a55128dff7ad86a6fdb98667b180b715b4ea377a04eb7d23b2d3a7ad
|
|
4
|
+
data.tar.gz: 150e59464bdf3e5e67614ec986264ba846868d86030b996496b4ad6761f989e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b287b9ed3ff07aeae8c8535ea29b650a0d76c477f44a3301ab4eff6016ec40da85dcb496f6d2b51b3fb903a6b6ed0652cd883e2504e937dccb3eba1fac5edeb
|
|
7
|
+
data.tar.gz: 507eb9a963bf908ce24883776f892a23b205d39530691a11f29b90d375ad464fcde5102808a6c38b75745a2180f8dda80298c479131395bfd1a98b7bbe529cf6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,111 +1,142 @@
|
|
|
1
|
-
## Rails
|
|
1
|
+
## Rails 8.1.3 (March 24, 2026) ##
|
|
2
2
|
|
|
3
|
-
*
|
|
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
|
|
6
|
+
## Rails 8.1.2.1 (March 23, 2026) ##
|
|
16
7
|
|
|
17
8
|
* No changes.
|
|
18
9
|
|
|
19
10
|
|
|
20
|
-
## Rails
|
|
11
|
+
## Rails 8.1.2 (January 08, 2026) ##
|
|
21
12
|
|
|
22
|
-
*
|
|
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
|
-
|
|
20
|
+
*OuYangJinTing*
|
|
26
21
|
|
|
27
|
-
*
|
|
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
|
|
27
|
+
## Rails 8.1.1 (October 28, 2025) ##
|
|
31
28
|
|
|
32
|
-
*
|
|
29
|
+
* Only index new serializers.
|
|
33
30
|
|
|
31
|
+
*Jesse Sharps*
|
|
34
32
|
|
|
35
|
-
## Rails 7.2.1.1 (October 15, 2024) ##
|
|
36
33
|
|
|
37
|
-
|
|
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
|
-
|
|
50
|
+
*Adrianna Chang*
|
|
41
51
|
|
|
42
|
-
*
|
|
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
|
-
|
|
58
|
+
* Remove deprecated internal `SuckerPunch` adapter in favor of the adapter included with the `sucker_punch` gem.
|
|
46
59
|
|
|
47
|
-
*
|
|
60
|
+
*Rafael Mendonça França*
|
|
48
61
|
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
the `TestAdapter` will continue to be used.
|
|
64
|
+
*Rafael Mendonça França*
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
* Remove deprecated `Rails.application.config.active_job.enqueue_after_transaction_commit`.
|
|
57
67
|
|
|
58
|
-
*
|
|
68
|
+
*Rafael Mendonça França*
|
|
59
69
|
|
|
60
|
-
*
|
|
70
|
+
* `ActiveJob::Serializers::ObjectSerializers#klass` method is now public.
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
Topic.transaction do
|
|
68
|
-
topic = Topic.create(...)
|
|
69
|
-
NewTopicNotificationJob.perform_later(topic)
|
|
70
|
-
end
|
|
71
|
-
```
|
|
75
|
+
*Jean Boussier*
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
and drop the job if the transaction is rolled back.
|
|
77
|
+
* Allow jobs to the interrupted and resumed with Continuations
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
|
|
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
|
-
*
|
|
114
|
+
*Donal McBreen*
|
|
86
115
|
|
|
87
|
-
*
|
|
116
|
+
* Defer invocation of ActiveJob enqueue callbacks until after commit when
|
|
117
|
+
`enqueue_after_transaction_commit` is enabled.
|
|
88
118
|
|
|
89
|
-
*
|
|
119
|
+
*Will Roever*
|
|
90
120
|
|
|
91
|
-
*
|
|
121
|
+
* Add `report:` option to `ActiveJob::Base#retry_on` and `#discard_on`
|
|
92
122
|
|
|
93
|
-
|
|
123
|
+
When the `report:` option is passed, errors will be reported to the error reporter
|
|
124
|
+
before being retried / discarded.
|
|
94
125
|
|
|
95
|
-
*
|
|
126
|
+
*Andrew Novoselac*
|
|
96
127
|
|
|
97
|
-
|
|
128
|
+
* Accept a block for `ActiveJob::ConfiguredJob#perform_later`.
|
|
98
129
|
|
|
99
|
-
|
|
130
|
+
This was inconsistent with a regular `ActiveJob::Base#perform_later`.
|
|
100
131
|
|
|
101
|
-
*
|
|
132
|
+
*fatkodima*
|
|
102
133
|
|
|
103
|
-
*
|
|
134
|
+
* Raise a more specific error during deserialization when a previously serialized job class is now unknown.
|
|
104
135
|
|
|
105
|
-
|
|
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
|
-
*
|
|
108
|
-
|
|
109
|
-
*Rafael Mendonça França*
|
|
140
|
+
*Earlopain*
|
|
110
141
|
|
|
111
|
-
Please check [
|
|
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
|
|
93
|
-
|
|
94
|
-
their gem, or as a stand-alone gem.
|
|
95
|
-
|
|
96
|
-
|
|
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
|
data/lib/active_job/arguments.rb
CHANGED
|
@@ -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
|
|
161
|
+
when RESERVED_KEYS
|
|
163
162
|
raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}")
|
|
164
|
-
when String
|
|
165
|
-
key
|
|
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] =
|
|
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
|
data/lib/active_job/base.rb
CHANGED
|
@@ -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/
|
|
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
|
|
75
|
-
include Translation
|
|
74
|
+
include ExecutionState
|
|
76
75
|
|
|
77
76
|
ActiveSupport.run_load_hooks(:active_job, self)
|
|
78
77
|
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
|