job-iteration 1.14.0 → 1.15.0
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 +6 -0
- data/README.md +21 -5
- data/lib/job-iteration/iteration.rb +65 -8
- data/lib/job-iteration/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dbb861f47602c89b8163b0f36f662c8b1f10c34528c0d06b0ce74918973e89db
|
|
4
|
+
data.tar.gz: b00adea778b0a722dc727fdd4a9954c60442df995ee273ec82ff9d85b9fa222f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d085184d274fcc7315277c0e11e45093674e96a152d6bf49eff6644be23974df17f6d778e0abf0d1289ac2203af8ea59a9c7a7d9aeedd46de26b6ed16649d86
|
|
7
|
+
data.tar.gz: 50fea852189c3f13f4b0c84f465f532e4b27804d5d71238f044fa0f6beec67c8372c83561b0690e1605eb09c47a3f07cfad24a417a3dd612a4b57bfda840696d
|
data/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,12 @@ nil
|
|
|
16
16
|
|
|
17
17
|
nil
|
|
18
18
|
|
|
19
|
+
## v1.15.0 (Jun 4, 2026)
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- [715](https://github.com/Shopify/job-iteration/pull/715) - Add support for keyword arguments in `build_enumerator` and `each_iteration` while preserving positional params Hash compatibility for existing jobs. This compatibility path is transitional; migrate jobs away from positional params Hash signatures paired with `perform_later(keyword: value)`.
|
|
24
|
+
|
|
19
25
|
## v1.14.0 (May 14, 2026)
|
|
20
26
|
|
|
21
27
|
### Breaking Changes
|
data/README.md
CHANGED
|
@@ -101,6 +101,21 @@ class BatchesAsRelationJob < ApplicationJob
|
|
|
101
101
|
end
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
```ruby
|
|
105
|
+
class ParallelIterationJob < ApplicationJob
|
|
106
|
+
include JobIteration::Iteration
|
|
107
|
+
|
|
108
|
+
def build_enumerator(cursor:)
|
|
109
|
+
enumerator_builder.parallel_active_record_on_records(User.all, instances: 5, cursor: cursor)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Runs in 5 separate jobs, with each job processing a subset of the users
|
|
113
|
+
def each_iteration(user)
|
|
114
|
+
user.notify_about_something
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
104
119
|
```ruby
|
|
105
120
|
class ArrayJob < ApplicationJob
|
|
106
121
|
include JobIteration::Iteration
|
|
@@ -178,20 +193,21 @@ Support for older platforms that have reached end of life may occasionally be dr
|
|
|
178
193
|
* [Best practices](guides/best-practices.md)
|
|
179
194
|
* [Writing custom enumerator](guides/custom-enumerator.md)
|
|
180
195
|
* [Throttling](guides/throttling.md)
|
|
196
|
+
* [Parallel iteration](guides/parallel-iteration.md)
|
|
181
197
|
|
|
182
198
|
For more detailed documentation, see [rubydoc](https://www.rubydoc.info/gems/job-iteration).
|
|
183
199
|
|
|
184
200
|
## Requirements
|
|
185
201
|
|
|
186
|
-
|
|
202
|
+
Active Job is the primary requirement for Iteration. For iteration without Active Job in Sidekiq, see [Sidekiq Iteration](https://github.com/sidekiq/sidekiq/wiki/Iteration).
|
|
187
203
|
|
|
188
204
|
### API
|
|
189
205
|
|
|
190
|
-
Iteration job must respond to `build_enumerator` and `each_iteration` methods. `build_enumerator` must return [Enumerator](http://ruby-doc.org/core-2.5.1/Enumerator.html) object that respects the `cursor` value.
|
|
206
|
+
Iteration job must respond to `build_enumerator` and `each_iteration` methods. `build_enumerator` must return an [Enumerator](http://ruby-doc.org/core-2.5.1/Enumerator.html) object that respects the `cursor` value.
|
|
191
207
|
|
|
192
208
|
### Sidekiq adapter
|
|
193
209
|
|
|
194
|
-
|
|
210
|
+
Running iterating jobs on Sidekiq should work with the default configuration. The most important setting is Sidekiq's [timeout](https://github.com/mperham/sidekiq/wiki/Deployment#overview) option, which defaults to 25 seconds. That allows the last `each_iteration` to complete and gracefully shutdown.
|
|
195
211
|
|
|
196
212
|
### Resque adapter
|
|
197
213
|
|
|
@@ -203,11 +219,11 @@ There a few configuration assumptions that are required for Iteration to work wi
|
|
|
203
219
|
|
|
204
220
|
**What happens when my job is interrupted?** A checkpoint will be persisted to Redis after the current `each_iteration`, and the job will be re-enqueued. Once it's popped off the queue, the worker will work off from the next iteration.
|
|
205
221
|
|
|
206
|
-
**What happens with retries?** An interruption of a job does not count as a retry. If an exception occurs, the job will retry or be discarded as normal using Active Job configuration for the job. If the job retries, it processes the iteration that originally failed and progress will continue from there on if successful.
|
|
222
|
+
**What happens with retries?** An interruption of a job does not count as a retry. If an exception occurs, the job will retry or be discarded as normal using Active Job configuration for the job. If the job retries, it re-processes the iteration that originally failed and progress will continue from there on if successful.
|
|
207
223
|
|
|
208
224
|
**What happens if my iteration takes a long time?** We recommend that a single `each_iteration` should take no longer than 30 seconds. In the future, this may raise an exception.
|
|
209
225
|
|
|
210
|
-
**Why is it important that `each_iteration`
|
|
226
|
+
**Why is it important that `each_iteration` runs quickly?** When the job worker is scheduled for restart or shutdown, it gets a notice to finish remaining unit of work. To guarantee that no progress is lost we need to make sure that `each_iteration` completes within a reasonable amount of time. The exact timeout depends on your queue adapter configuration.
|
|
211
227
|
|
|
212
228
|
**Why do I use have to use this ugly helper in `build_enumerator`? Why can't you automatically infer it?** This is how the first version of the API worked. We checked the type of object returned by `build_enumerable`, and whether it was ActiveRecord Relation or an Array, we used the matching adapter. This caused opaque type branching in Iteration internals and it didn’t allow developers to craft their own Enumerators and control the cursor value. We made a decision to _always_ return Enumerator instance from `build_enumerator`. Now we provide explicit helpers to convert ActiveRecord Relation or an Array to Enumerator, and for more complex iteration flows developers can build their own `Enumerator` objects.
|
|
213
229
|
|
|
@@ -107,8 +107,8 @@ module JobIteration
|
|
|
107
107
|
self.total_time = Float(job_data["total_time"] || 0.0)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def perform(
|
|
111
|
-
interruptible_perform(
|
|
110
|
+
def perform(...) # @private
|
|
111
|
+
interruptible_perform(...)
|
|
112
112
|
|
|
113
113
|
nil
|
|
114
114
|
end
|
|
@@ -128,12 +128,12 @@ module JobIteration
|
|
|
128
128
|
JobIteration.enumerator_builder.new(self)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
def interruptible_perform(*
|
|
131
|
+
def interruptible_perform(*args, **kwargs)
|
|
132
132
|
self.start_time = Time.now.utc
|
|
133
133
|
|
|
134
134
|
enumerator = nil
|
|
135
135
|
ActiveSupport::Notifications.instrument("build_enumerator.iteration", instrumentation_tags) do
|
|
136
|
-
enumerator =
|
|
136
|
+
enumerator = call_job_iteration_build_enumerator(args, kwargs)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
unless enumerator
|
|
@@ -161,7 +161,7 @@ module JobIteration
|
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
completed = catch(:abort) do
|
|
164
|
-
iterate_with_enumerator(enumerator,
|
|
164
|
+
iterate_with_enumerator(enumerator, args, kwargs)
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
run_callbacks(:shutdown)
|
|
@@ -178,8 +178,7 @@ module JobIteration
|
|
|
178
178
|
end
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
-
def iterate_with_enumerator(enumerator,
|
|
182
|
-
arguments = arguments.dup.freeze
|
|
181
|
+
def iterate_with_enumerator(enumerator, args, kwargs)
|
|
183
182
|
found_record = false
|
|
184
183
|
@needs_reenqueue = false
|
|
185
184
|
|
|
@@ -191,7 +190,7 @@ module JobIteration
|
|
|
191
190
|
ActiveSupport::Notifications.instrument("each_iteration.iteration", tags) do
|
|
192
191
|
found_record = true
|
|
193
192
|
run_callbacks(:iterate) do
|
|
194
|
-
|
|
193
|
+
call_job_iteration_each_iteration(object_from_enumerator, args, kwargs)
|
|
195
194
|
end
|
|
196
195
|
self.cursor_position = cursor_from_enumerator
|
|
197
196
|
end
|
|
@@ -215,6 +214,64 @@ module JobIteration
|
|
|
215
214
|
adjust_total_time
|
|
216
215
|
end
|
|
217
216
|
|
|
217
|
+
def call_job_iteration_build_enumerator(args, kwargs)
|
|
218
|
+
positional_args, keyword_args = normalize_job_iteration_arguments(args, kwargs, :build_enumerator)
|
|
219
|
+
|
|
220
|
+
if keyword_args&.key?(:cursor)
|
|
221
|
+
raise ArgumentError, "The keyword argument `cursor` is reserved for the job iteration framework. " \
|
|
222
|
+
"Please remove `cursor` from the arguments passed to the job or rename it"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# `keyword_args || {}` because splatting `nil` raises on Ruby < 3.4; it only
|
|
226
|
+
# became a no-op in 3.4 (https://bugs.ruby-lang.org/issues/20064).
|
|
227
|
+
build_enumerator(*positional_args, **(keyword_args || {}), cursor: cursor_position)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def call_job_iteration_each_iteration(object_from_enumerator, args, kwargs)
|
|
231
|
+
positional_args, keyword_args = normalize_job_iteration_arguments(args, kwargs, :each_iteration)
|
|
232
|
+
# `keyword_args || {}` because splatting `nil` raises on Ruby < 3.4; it only
|
|
233
|
+
# became a no-op in 3.4 (https://bugs.ruby-lang.org/issues/20064).
|
|
234
|
+
each_iteration(object_from_enumerator, *positional_args, **(keyword_args || {}))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Normalize Active Job kwargs for job-iteration dispatch. If the target method
|
|
238
|
+
# accepts job keyword parameters other than build_enumerator's reserved cursor:,
|
|
239
|
+
# keep kwargs separate so they can be splatted. Otherwise, append kwargs as a
|
|
240
|
+
# positional params Hash for transitional compatibility with existing jobs
|
|
241
|
+
# enqueued with keyword syntax.
|
|
242
|
+
#: (Array[top], Hash[Symbol, top], Symbol) -> [Array[top], Hash[Symbol, top]?]
|
|
243
|
+
def normalize_job_iteration_arguments(args, kwargs, method_name)
|
|
244
|
+
if kwargs.empty?
|
|
245
|
+
[args.dup.freeze, nil]
|
|
246
|
+
elsif job_has_keyword_parameters?(method_name)
|
|
247
|
+
[args.dup.freeze, kwargs.dup.freeze]
|
|
248
|
+
else
|
|
249
|
+
params_hash_args = args + [kwargs]
|
|
250
|
+
[params_hash_args.dup.freeze, nil]
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
#: (Symbol) -> bool
|
|
255
|
+
def job_has_keyword_parameters?(method_name)
|
|
256
|
+
@job_has_keyword_parameters ||= {}
|
|
257
|
+
return @job_has_keyword_parameters[method_name] if @job_has_keyword_parameters.key?(method_name)
|
|
258
|
+
|
|
259
|
+
@job_has_keyword_parameters[method_name] = method_parameters(method_name).any? do |type, name|
|
|
260
|
+
job_keyword_argument?(method_name, type, name)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def job_keyword_argument?(method_name, type, name)
|
|
265
|
+
# Match keyword parameters: double-splat (**kwargs), required (key:) or optional (key: default).
|
|
266
|
+
return false unless type == :keyrest || type == :keyreq || type == :key
|
|
267
|
+
|
|
268
|
+
# Ignore the `cursor:` argument, which is part of the `job-iteration`
|
|
269
|
+
# framework, and not a argument in the job's serialized argument list.
|
|
270
|
+
return false if method_name == :build_enumerator && name == :cursor
|
|
271
|
+
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
|
|
218
275
|
def reenqueue_iteration_job
|
|
219
276
|
ActiveSupport::Notifications.instrument(
|
|
220
277
|
"interrupted.iteration",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: job-iteration
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
79
|
version: '0'
|
|
80
80
|
requirements: []
|
|
81
|
-
rubygems_version: 4.0.
|
|
81
|
+
rubygems_version: 4.0.12
|
|
82
82
|
specification_version: 4
|
|
83
83
|
summary: Makes your background jobs interruptible and resumable.
|
|
84
84
|
test_files: []
|