job-iteration 1.15.1 → 1.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c185ebebe932e13f51e39bc16d6bd18eca716a6bb8b179d400d0ab2cfff31910
4
- data.tar.gz: 8675ef2c1d85051d2166ef65b870c4982bb2f3e9657c513b28dbadf69de1588e
3
+ metadata.gz: 657313f73892dc576ba595499f00d97a211936bdbec570db64e72c7431aa0a11
4
+ data.tar.gz: f12f068e6a83efc6fd702e4c5068c8b428587ec2b94eb9087c00e7141f689dee
5
5
  SHA512:
6
- metadata.gz: 82b50b5701c001350571cf718bad50a7e0c882bd3e251f1c016ab0e46e4b224034a0a8a6042262254bb356c0d2b291eba69b437ee659282ca1d0a798a50fae54
7
- data.tar.gz: 72abfc4ee81110b8d99ba8d5ccc9cdad7cd32de945ebc0a4a1022ecd8bc8307196c0889948e7f7ef1ceff8a78f5cbfd579243be59b3e6534d87d673c11149021
6
+ metadata.gz: dd60d3982a00af5e2193739c0e9e62adee8e08414628fa19a9f1d6d85f4ad1c5d9cbbd3b5a931b695fccdd50d60a5592a420a04c6eaa446f3b5cbf2e53beadb9
7
+ data.tar.gz: 0e4c271a6a72a11bafcb5bb3fd362f88c2a90ee8b1ddb622c7053c0b0a8eed6b78b2420db0a28d448d7ce12026ad6313d409441567b30acc8e46a46a3243cfdc
data/CHANGELOG.md CHANGED
@@ -16,6 +16,19 @@ nil
16
16
 
17
17
  nil
18
18
 
19
+ ## v1.17.0 (Oct 27, 2026)
20
+
21
+ ### Features
22
+
23
+ - [753](https://github.com/Shopify/job-iteration/pull/753) - Custom enumerator builders can override the protected `default_tolerated_enqueue_errors` hook to set the default parallel child-admission policy. Explicit arrays still replace the default, including `[]` for strict behavior.
24
+
25
+ ## v1.16.0 (Oct 20, 2026)
26
+
27
+ ### Features
28
+
29
+ - [746](https://github.com/Shopify/job-iteration/pull/746) - `build_parallel_enumerator` accepts `tolerated_enqueue_errors:`, a list of enqueue error classes that do not fail the parent job. A child job that fails to enqueue with one of them is skipped and reported through the `skipped_parallel_jobs.iteration` event.
30
+ - [747](https://github.com/Shopify/job-iteration/pull/747) - Support running iteration jobs in non-main Ractors. `JobIteration.max_job_runtime=` and `JobIteration.default_retry_backoff=` now make their values Ractor-shareable, which deep-freezes them.
31
+
19
32
  ## v1.15.1 (Sep 15, 2026)
20
33
 
21
34
  ### Bug fixes
@@ -233,12 +233,19 @@ module JobIteration
233
233
  wrap(self, enum)
234
234
  end
235
235
 
236
- def build_parallel_enumerator(instances:, cursor:, &block)
236
+ def build_parallel_enumerator(instances:, cursor:, tolerated_enqueue_errors: default_tolerated_enqueue_errors, &block)
237
237
  unless instances.is_a?(Integer) && instances.positive?
238
238
  raise ArgumentError, "instances must be a positive Integer"
239
239
  end
240
240
 
241
- return ParallelEnumerator::EnqueueJobs.new(instances) if cursor.nil?
241
+ unless tolerated_enqueue_errors.is_a?(Array) &&
242
+ tolerated_enqueue_errors.all? { |error_class| error_class.is_a?(Class) && error_class <= Exception }
243
+ raise ArgumentError, "tolerated_enqueue_errors must be an Array of Exception classes"
244
+ end
245
+
246
+ if cursor.nil?
247
+ return ParallelEnumerator::EnqueueJobs.new(instances, tolerated_enqueue_errors: tolerated_enqueue_errors)
248
+ end
242
249
 
243
250
  enum = ParallelEnumerator.new(block, cursor: cursor).to_enum
244
251
  wrap(self, enum)
@@ -259,6 +266,14 @@ module JobIteration
259
266
  alias_method :nested, :build_nested_enumerator
260
267
  alias_method :parallel, :build_parallel_enumerator
261
268
 
269
+ protected
270
+
271
+ # Override to supply the policy used when tolerated_enqueue_errors is omitted.
272
+ # Explicit arrays replace this default; an empty array keeps enqueue failures strict.
273
+ def default_tolerated_enqueue_errors
274
+ []
275
+ end
276
+
262
277
  private
263
278
 
264
279
  def build_active_record_enumerator(scope, cursor:, instance: nil, instances: nil, **args)
@@ -34,7 +34,8 @@ module JobIteration
34
34
  def register(name, adapter)
35
35
  raise ArgumentError, "adapter must be callable" unless adapter.respond_to?(:call)
36
36
 
37
- registry[name.to_sym] = adapter
37
+ @registry = registry.merge(name.to_sym => adapter).freeze
38
+ nil
38
39
  end
39
40
 
40
41
  private
@@ -42,7 +43,7 @@ module JobIteration
42
43
  attr_reader :registry
43
44
  end
44
45
 
45
- @registry = {}
46
+ @registry = {}.freeze
46
47
 
47
48
  # Built-in Rails adapters. It doesn't make sense to interrupt for these.
48
49
  register(:async, NullAdapter)
@@ -345,7 +345,7 @@ module JobIteration
345
345
  def method_parameters(method_name)
346
346
  method = method(method_name)
347
347
 
348
- if defined?(T::Private::Methods)
348
+ if defined?(T::Private::Methods) && Ractor.current == Ractor.main
349
349
  signature = T::Private::Methods.signature_for_method(method)
350
350
  method = signature.method if signature
351
351
  end
@@ -21,7 +21,15 @@ module JobIteration
21
21
 
22
22
  def enqueue_parallel_jobs(event)
23
23
  info do
24
- "[JobIteration::Iteration] Enqueued #{event.payload[:instances]} parallel jobs."
24
+ "[JobIteration::Iteration] Attempted to enqueue #{event.payload[:instances]} parallel jobs."
25
+ end
26
+ end
27
+
28
+ def skipped_parallel_jobs(event)
29
+ info do
30
+ "[JobIteration::Iteration] Skipped #{event.payload[:skipped_instances].size} of " \
31
+ "#{event.payload[:instances]} parallel jobs that failed to enqueue with a tolerated error. " \
32
+ "skipped_instances=#{event.payload[:skipped_instances]} enqueue_errors=#{event.payload[:enqueue_errors].uniq}"
25
33
  end
26
34
  end
27
35
 
@@ -7,11 +7,12 @@ module JobIteration
7
7
  class EnqueueError < StandardError; end
8
8
 
9
9
  class EnqueueJobs
10
- def initialize(instances)
10
+ def initialize(instances, tolerated_enqueue_errors: [])
11
11
  @instances = instances
12
+ @tolerated_enqueue_errors = tolerated_enqueue_errors
12
13
  end
13
14
 
14
- attr_reader :instances
15
+ attr_reader :instances, :tolerated_enqueue_errors
15
16
 
16
17
  def enqueue_jobs(job)
17
18
  child_jobs = instances.times.map do |index|
@@ -26,10 +27,30 @@ module JobIteration
26
27
 
27
28
  ActiveJob.perform_all_later(child_jobs)
28
29
 
29
- unless child_jobs.all?(&:successfully_enqueued?)
30
- failed_count = instances - child_jobs.count(&:successfully_enqueued?)
31
- raise EnqueueError, "Failed to enqueue #{failed_count} out of #{instances} child jobs"
32
- end
30
+ failed_jobs = child_jobs.reject(&:successfully_enqueued?)
31
+ return if failed_jobs.empty?
32
+
33
+ skipped_jobs, failed_jobs = failed_jobs.partition { |child_job| tolerated?(child_job.enqueue_error) }
34
+ instrument_skipped_jobs(job, skipped_jobs) if skipped_jobs.any?
35
+ return if failed_jobs.empty?
36
+
37
+ raise EnqueueError, "Failed to enqueue #{failed_jobs.size} out of #{instances} child jobs"
38
+ end
39
+
40
+ private
41
+
42
+ def tolerated?(enqueue_error)
43
+ tolerated_enqueue_errors.any? { |error_class| enqueue_error.is_a?(error_class) }
44
+ end
45
+
46
+ def instrument_skipped_jobs(job, skipped_jobs)
47
+ ActiveSupport::Notifications.instrument(
48
+ "skipped_parallel_jobs.iteration",
49
+ job_class: job.class.name,
50
+ instances: instances,
51
+ skipped_instances: skipped_jobs.map { |child_job| child_job.cursor_position.fetch("instance") },
52
+ enqueue_errors: skipped_jobs.map { |child_job| child_job.enqueue_error.class.name },
53
+ )
33
54
  end
34
55
  end
35
56
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JobIteration
4
- VERSION = "1.15.1"
4
+ VERSION = "1.17.0"
5
5
  end
data/lib/job-iteration.rb CHANGED
@@ -45,7 +45,11 @@ module JobIteration
45
45
  # class ChildJob < MyJob
46
46
  # self.job_iteration_max_job_runtime = 3.minutes # MyJob's 1.minute will be discarded.
47
47
  # # ...
48
- attr_accessor :max_job_runtime
48
+ attr_reader :max_job_runtime
49
+
50
+ def max_job_runtime=(max_job_runtime)
51
+ @max_job_runtime = Ractor.make_shareable(max_job_runtime)
52
+ end
49
53
 
50
54
  # Configures a delay duration to wait before resuming an interrupted job.
51
55
  # @example
@@ -55,7 +59,11 @@ module JobIteration
55
59
  # Defaults to nil which means interrupted jobs will be retried immediately.
56
60
  # This value will be ignored when an interruption is raised by a throttle enumerator,
57
61
  # where the throttle backoff value will take precedence over this setting.
58
- attr_accessor :default_retry_backoff
62
+ attr_reader :default_retry_backoff
63
+
64
+ def default_retry_backoff=(default_retry_backoff)
65
+ @default_retry_backoff = Ractor.make_shareable(default_retry_backoff)
66
+ end
59
67
 
60
68
  attr_reader :interruption_adapter
61
69
 
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.15.1
4
+ version: 1.17.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.21
81
+ rubygems_version: 4.0.23
82
82
  specification_version: 4
83
83
  summary: Makes your background jobs interruptible and resumable.
84
84
  test_files: []