job-iteration 1.13.1 → 1.14.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 +16 -0
- data/README.md +1 -1
- data/job-iteration.gemspec +1 -1
- data/lib/job-iteration/active_record_batch_enumerator.rb +0 -4
- data/lib/job-iteration/active_record_cursor.rb +13 -2
- data/lib/job-iteration/active_record_enumerator.rb +6 -3
- data/lib/job-iteration/enumerator_builder.rb +70 -1
- data/lib/job-iteration/iteration.rb +8 -0
- data/lib/job-iteration/log_subscriber.rb +6 -0
- data/lib/job-iteration/parallel_enumerator.rb +52 -0
- data/lib/job-iteration/railtie.rb +1 -2
- data/lib/job-iteration/version.rb +1 -1
- data/lib/tapioca/dsl/compilers/job_iteration.rb +5 -9
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6cd8bb708ce42d3333f9631f344bb33b506cd2e3d0fc8a5d73882f2f52a811a
|
|
4
|
+
data.tar.gz: 30b9ece97b5e981a61fbef2110e60fdffcbd95dfdeec14def3652f9384f7e4d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc9bd924dfbce3e582356a9b29edec62ca597c6b50cad39afb29970e9d1b1ce799678797b7441b6aa6732d25e8b5e8fd896f88b7dd118cae552f5acc50a31143
|
|
7
|
+
data.tar.gz: 532e91769910a4172d8f7773bfd59b3fa54372fed4b9fd22d6309afa0861a626a878dedb50972127658a76e90b651cf1837a7d87fc61deebc81655ee418698d9
|
data/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,22 @@ nil
|
|
|
16
16
|
|
|
17
17
|
nil
|
|
18
18
|
|
|
19
|
+
## v1.14.0 (May 14, 2026)
|
|
20
|
+
|
|
21
|
+
### Breaking Changes
|
|
22
|
+
|
|
23
|
+
- [704](https://github.com/Shopify/job-iteration/pull/704) - Drop support for Rails 7.0. The minimum supported Rails version is now 7.1.
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- [702](https://github.com/Shopify/job-iteration/pull/702) - Add support for parallel iteration with `enumerator_builder.parallel`. This enqueues multiple jobs, allowing you to split up the work across multiple instances.
|
|
28
|
+
- [705](https://github.com/Shopify/job-iteration/pull/705) - Add support for parallel array iteration with `enumerator_builder.parallel_array`.
|
|
29
|
+
- [706](https://github.com/Shopify/job-iteration/pull/706) - Add support for parallel Active Record relation iteration with `enumerator_builder.parallel_active_record_on_records` and `enumerator_builder.parallel_active_record_on_batches`.
|
|
30
|
+
|
|
31
|
+
### Bug fixes
|
|
32
|
+
|
|
33
|
+
- [709](https://github.com/Shopify/job-iteration/pull/709) - Fix an issue with parallel iteration when `instances` changed after child jobs were enqueued.
|
|
34
|
+
|
|
19
35
|
## v1.13.1 (Apr 28, 2026)
|
|
20
36
|
|
|
21
37
|
### Bug fixes
|
data/README.md
CHANGED
|
@@ -167,7 +167,7 @@ Job-iteration currently supports the following queue adapters (in order of imple
|
|
|
167
167
|
It supports the following platforms:
|
|
168
168
|
|
|
169
169
|
- Ruby 3.1 and later
|
|
170
|
-
- Rails 7.
|
|
170
|
+
- Rails 7.1 and later
|
|
171
171
|
|
|
172
172
|
Support for older platforms that have reached end of life may occasionally be dropped if maintaining backwards compatibility is cumbersome.
|
|
173
173
|
|
data/job-iteration.gemspec
CHANGED
|
@@ -26,5 +26,5 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.metadata["changelog_uri"] = "https://github.com/Shopify/job-iteration/blob/main/CHANGELOG.md"
|
|
27
27
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
28
28
|
|
|
29
|
-
spec.add_dependency("activejob", ">= 7.
|
|
29
|
+
spec.add_dependency("activejob", ">= 7.1")
|
|
30
30
|
end
|
|
@@ -66,10 +66,6 @@ module JobIteration
|
|
|
66
66
|
pkey = @column_mgr.primary_key
|
|
67
67
|
pkey_values = primary_key_values
|
|
68
68
|
|
|
69
|
-
# If the primary key is only composed of a single column, simplify the
|
|
70
|
-
# query. This keeps us compatible with Rails prior to 7.1 where composite
|
|
71
|
-
# primary keys were introduced along with the syntax that allows you to
|
|
72
|
-
# query for multi-column values.
|
|
73
69
|
if pkey.size <= 1
|
|
74
70
|
pkey = pkey.first
|
|
75
71
|
pkey_values = pkey_values.map(&:first)
|
|
@@ -18,7 +18,7 @@ module JobIteration
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def initialize(relation, columns
|
|
21
|
+
def initialize(relation, columns, position, instance, instances)
|
|
22
22
|
@columns = if columns
|
|
23
23
|
Array(columns)
|
|
24
24
|
else
|
|
@@ -35,6 +35,17 @@ module JobIteration
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
@base_relation = relation.reorder(@columns.join(","))
|
|
38
|
+
|
|
39
|
+
if instances.present?
|
|
40
|
+
pk = relation.primary_key
|
|
41
|
+
unless pk.is_a?(String) && relation.klass.column_for_attribute(pk).type == :integer
|
|
42
|
+
raise ArgumentError, "Parallel iteration requires a single integer primary key. " \
|
|
43
|
+
"For more complex cases, use the enumerator_builder.parallel primitive directly."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@base_relation = @base_relation.where("#{relation.table_name}.#{pk} % ? = ?", instances, instance)
|
|
47
|
+
end
|
|
48
|
+
|
|
38
49
|
@reached_end = false
|
|
39
50
|
end
|
|
40
51
|
|
|
@@ -56,7 +67,7 @@ module JobIteration
|
|
|
56
67
|
self.position = @columns.map do |column|
|
|
57
68
|
method = column.to_s.split(".").last
|
|
58
69
|
|
|
59
|
-
if
|
|
70
|
+
if method == "id"
|
|
60
71
|
record.id_value
|
|
61
72
|
else
|
|
62
73
|
record.send(method.to_sym)
|
|
@@ -7,7 +7,7 @@ module JobIteration
|
|
|
7
7
|
class ActiveRecordEnumerator
|
|
8
8
|
SQL_DATETIME_WITH_NSEC = "%Y-%m-%d %H:%M:%S.%N"
|
|
9
9
|
|
|
10
|
-
def initialize(relation, columns: nil, batch_size: 100, timezone: nil, cursor: nil)
|
|
10
|
+
def initialize(relation, columns: nil, batch_size: 100, timezone: nil, cursor: nil, instance: nil, instances: nil)
|
|
11
11
|
@relation = relation
|
|
12
12
|
@batch_size = batch_size
|
|
13
13
|
@timezone = timezone
|
|
@@ -17,6 +17,8 @@ module JobIteration
|
|
|
17
17
|
Array(relation.primary_key).map { |pk| "#{relation.table_name}.#{pk}" }
|
|
18
18
|
end
|
|
19
19
|
@cursor = cursor
|
|
20
|
+
@instance = instance
|
|
21
|
+
@instances = instances
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def records
|
|
@@ -39,7 +41,8 @@ module JobIteration
|
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def size
|
|
42
|
-
@relation.count(:all)
|
|
44
|
+
full_size = @relation.count(:all)
|
|
45
|
+
@instances.present? ? full_size / @instances : full_size
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
private
|
|
@@ -61,7 +64,7 @@ module JobIteration
|
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
def finder_cursor
|
|
64
|
-
JobIteration::ActiveRecordCursor.new(@relation, @columns, @cursor)
|
|
67
|
+
JobIteration::ActiveRecordCursor.new(@relation, @columns, @cursor, @instance, @instances)
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
def column_value(record, attribute)
|
|
@@ -5,6 +5,7 @@ require_relative "active_record_enumerator"
|
|
|
5
5
|
require_relative "csv_enumerator"
|
|
6
6
|
require_relative "throttle_enumerator"
|
|
7
7
|
require_relative "nested_enumerator"
|
|
8
|
+
require_relative "parallel_enumerator"
|
|
8
9
|
require "forwardable"
|
|
9
10
|
|
|
10
11
|
module JobIteration
|
|
@@ -65,6 +66,23 @@ module JobIteration
|
|
|
65
66
|
wrap(self, enumerable.each_with_index.drop(drop).to_enum { enumerable.size - drop })
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Builds an Enumerator that iterates over a given array, across +instances+ parallel jobs.
|
|
70
|
+
#
|
|
71
|
+
# Child job i iterates over the slice of the array starting at
|
|
72
|
+
# index (array.size / instances * i).floor and ending at index (array.size / instances * (i + 1)).floor - 1.
|
|
73
|
+
def build_parallel_array_enumerator(array, instances:, cursor:)
|
|
74
|
+
unless array.is_a?(Array)
|
|
75
|
+
raise ArgumentError, "array must be an Array"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
build_parallel_enumerator(instances: instances, cursor: cursor) do |instance, instances, inner_cursor|
|
|
79
|
+
slice_start = (array.size.to_f / instances * instance).floor
|
|
80
|
+
next_slice_start = (array.size.to_f / instances * (instance + 1)).floor
|
|
81
|
+
slice = array[slice_start...next_slice_start]
|
|
82
|
+
build_array_enumerator(slice, cursor: inner_cursor)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
68
86
|
# Builds Enumerator from Active Record Relation. Each Enumerator tick moves the cursor one row forward.
|
|
69
87
|
#
|
|
70
88
|
# +columns:+ argument is used to build the actual query for iteration. +columns+: defaults to primary key:
|
|
@@ -103,6 +121,21 @@ module JobIteration
|
|
|
103
121
|
wrap(self, enum)
|
|
104
122
|
end
|
|
105
123
|
|
|
124
|
+
# Builds an Enumerator that iterates over a given Active Record Relation, across +instances+ parallel jobs.
|
|
125
|
+
#
|
|
126
|
+
# Child job i iterates over the records where the id is equal to (instance % instances).
|
|
127
|
+
def build_parallel_active_record_enumerator_on_records(scope, instances:, cursor:, **args)
|
|
128
|
+
build_parallel_enumerator(instances: instances, cursor: cursor) do |instance, instances, inner_cursor|
|
|
129
|
+
build_active_record_enumerator(
|
|
130
|
+
scope,
|
|
131
|
+
cursor: inner_cursor,
|
|
132
|
+
instances: instances,
|
|
133
|
+
instance: instance,
|
|
134
|
+
**args,
|
|
135
|
+
).records
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
106
139
|
# Builds Enumerator from Active Record Relation and enumerates on batches of records.
|
|
107
140
|
# Each Enumerator tick moves the cursor +batch_size+ rows forward.
|
|
108
141
|
#
|
|
@@ -118,6 +151,21 @@ module JobIteration
|
|
|
118
151
|
wrap(self, enum)
|
|
119
152
|
end
|
|
120
153
|
|
|
154
|
+
# Builds an Enumerator that iterates over a given Active Record Relation, across +instances+ parallel jobs, and enumerates on batches.
|
|
155
|
+
#
|
|
156
|
+
# Child job i iterates over the batches of records where the id is equal to (instance % instances).
|
|
157
|
+
def build_parallel_active_record_enumerator_on_batches(scope, instances:, cursor:, **args)
|
|
158
|
+
build_parallel_enumerator(instances: instances, cursor: cursor) do |instance, instances, inner_cursor|
|
|
159
|
+
build_active_record_enumerator(
|
|
160
|
+
scope,
|
|
161
|
+
cursor: inner_cursor,
|
|
162
|
+
instances: instances,
|
|
163
|
+
instance: instance,
|
|
164
|
+
**args,
|
|
165
|
+
).batches
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
121
169
|
# Builds Enumerator from Active Record Relation and enumerates on batches, yielding Active Record Relations.
|
|
122
170
|
# See documentation for #build_active_record_enumerator_on_batches.
|
|
123
171
|
def build_active_record_enumerator_on_batch_relations(scope, wrap: true, cursor:, **args)
|
|
@@ -185,27 +233,48 @@ module JobIteration
|
|
|
185
233
|
wrap(self, enum)
|
|
186
234
|
end
|
|
187
235
|
|
|
236
|
+
def build_parallel_enumerator(instances:, cursor:, &block)
|
|
237
|
+
unless instances.is_a?(Integer) && instances.positive?
|
|
238
|
+
raise ArgumentError, "instances must be a positive Integer"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
return ParallelEnumerator::EnqueueJobs.new(instances) if cursor.nil?
|
|
242
|
+
|
|
243
|
+
enum = ParallelEnumerator.new(block, cursor: cursor).to_enum
|
|
244
|
+
wrap(self, enum)
|
|
245
|
+
end
|
|
246
|
+
|
|
188
247
|
alias_method :once, :build_once_enumerator
|
|
189
248
|
alias_method :times, :build_times_enumerator
|
|
190
249
|
alias_method :array, :build_array_enumerator
|
|
250
|
+
alias_method :parallel_array, :build_parallel_array_enumerator
|
|
191
251
|
alias_method :active_record_on_records, :build_active_record_enumerator_on_records
|
|
252
|
+
alias_method :parallel_active_record_on_records, :build_parallel_active_record_enumerator_on_records
|
|
192
253
|
alias_method :active_record_on_batches, :build_active_record_enumerator_on_batches
|
|
254
|
+
alias_method :parallel_active_record_on_batches, :build_parallel_active_record_enumerator_on_batches
|
|
193
255
|
alias_method :active_record_on_batch_relations, :build_active_record_enumerator_on_batch_relations
|
|
194
256
|
alias_method :throttle, :build_throttle_enumerator
|
|
195
257
|
alias_method :csv, :build_csv_enumerator
|
|
196
258
|
alias_method :csv_on_batches, :build_csv_enumerator_on_batches
|
|
197
259
|
alias_method :nested, :build_nested_enumerator
|
|
260
|
+
alias_method :parallel, :build_parallel_enumerator
|
|
198
261
|
|
|
199
262
|
private
|
|
200
263
|
|
|
201
|
-
def build_active_record_enumerator(scope, cursor:, **args)
|
|
264
|
+
def build_active_record_enumerator(scope, cursor:, instance: nil, instances: nil, **args)
|
|
202
265
|
unless scope.is_a?(ActiveRecord::Relation)
|
|
203
266
|
raise ArgumentError, "scope must be an ActiveRecord::Relation"
|
|
204
267
|
end
|
|
205
268
|
|
|
269
|
+
if (instance.nil? && instances.present?) || (instance.present? && instances.nil?)
|
|
270
|
+
raise ArgumentError, "instance and instances must both be provided or both be nil"
|
|
271
|
+
end
|
|
272
|
+
|
|
206
273
|
JobIteration::ActiveRecordEnumerator.new(
|
|
207
274
|
scope,
|
|
208
275
|
cursor: cursor,
|
|
276
|
+
instance: instance,
|
|
277
|
+
instances: instances,
|
|
209
278
|
**args,
|
|
210
279
|
)
|
|
211
280
|
end
|
|
@@ -141,6 +141,14 @@ module JobIteration
|
|
|
141
141
|
return
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
if enumerator.is_a?(ParallelEnumerator::EnqueueJobs)
|
|
145
|
+
tags = instrumentation_tags.merge(instances: enumerator.instances)
|
|
146
|
+
ActiveSupport::Notifications.instrument("enqueue_parallel_jobs.iteration", tags) do
|
|
147
|
+
enumerator.enqueue_jobs(self)
|
|
148
|
+
end
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
144
152
|
assert_enumerator!(enumerator)
|
|
145
153
|
|
|
146
154
|
if executions == 1 && times_interrupted == 0
|
|
@@ -19,6 +19,12 @@ module JobIteration
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def enqueue_parallel_jobs(event)
|
|
23
|
+
info do
|
|
24
|
+
"[JobIteration::Iteration] Enqueued #{event.payload[:instances]} parallel jobs."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
def interrupted(event)
|
|
23
29
|
info do
|
|
24
30
|
"[JobIteration::Iteration] Interrupting and re-enqueueing the job " \
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module JobIteration
|
|
5
|
+
# ParallelEnumerator allows you to parallelize iterations.
|
|
6
|
+
class ParallelEnumerator
|
|
7
|
+
class EnqueueError < StandardError; end
|
|
8
|
+
|
|
9
|
+
class EnqueueJobs
|
|
10
|
+
def initialize(instances)
|
|
11
|
+
@instances = instances
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :instances
|
|
15
|
+
|
|
16
|
+
def enqueue_jobs(job)
|
|
17
|
+
child_jobs = instances.times.map do |index|
|
|
18
|
+
job.class.new(*job.arguments).tap do |child_job|
|
|
19
|
+
child_job.cursor_position = { "instance" => index, "instances" => instances, "inner_cursor" => nil }
|
|
20
|
+
|
|
21
|
+
# Carry forward potential overrides from the parent job
|
|
22
|
+
child_job.queue_name = job.queue_name
|
|
23
|
+
child_job.priority = job.priority if job.priority
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
ActiveJob.perform_all_later(child_jobs)
|
|
28
|
+
|
|
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
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(block, cursor:)
|
|
37
|
+
@instance = cursor["instance"]
|
|
38
|
+
@instances = cursor["instances"]
|
|
39
|
+
inner_cursor = cursor["inner_cursor"]
|
|
40
|
+
@inner_enum = block.call(@instance, @instances, inner_cursor)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_enum
|
|
44
|
+
Enumerator.new(-> { @inner_enum.size }) do |yielder|
|
|
45
|
+
@inner_enum.each do |object_from_enumerator, cursor_from_enumerator|
|
|
46
|
+
parallel_cursor = { "instance" => @instance, "instances" => @instances, "inner_cursor" => cursor_from_enumerator }
|
|
47
|
+
yielder.yield(object_from_enumerator, parallel_cursor)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -5,8 +5,7 @@ return unless defined?(Rails::Railtie)
|
|
|
5
5
|
module JobIteration
|
|
6
6
|
class Railtie < Rails::Railtie
|
|
7
7
|
initializer "job_iteration.register_deprecator" do |app|
|
|
8
|
-
|
|
9
|
-
app.deprecators[:job_iteration] = JobIteration::Deprecation if app.respond_to?(:deprecators)
|
|
8
|
+
app.deprecators[:job_iteration] = JobIteration::Deprecation
|
|
10
9
|
end
|
|
11
10
|
end
|
|
12
11
|
end
|
|
@@ -105,15 +105,11 @@ module Tapioca
|
|
|
105
105
|
).returns(T::Array[RBI::TypedParam])
|
|
106
106
|
end
|
|
107
107
|
def perform_later_parameters(parameters, returned_job_class)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)]
|
|
114
|
-
else
|
|
115
|
-
parameters
|
|
116
|
-
end
|
|
108
|
+
parameters.reject! { |typed_param| RBI::BlockParam === typed_param.param }
|
|
109
|
+
parameters + [create_block_param(
|
|
110
|
+
"block",
|
|
111
|
+
type: "T.nilable(T.proc.params(job: #{returned_job_class}).void)",
|
|
112
|
+
)]
|
|
117
113
|
end
|
|
118
114
|
|
|
119
115
|
class << self
|
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.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '7.
|
|
18
|
+
version: '7.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '7.
|
|
25
|
+
version: '7.1'
|
|
26
26
|
description: Makes your background jobs interruptible and resumable.
|
|
27
27
|
email:
|
|
28
28
|
- ops-accounts+shipit@shopify.com
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- lib/job-iteration/iteration.rb
|
|
53
53
|
- lib/job-iteration/log_subscriber.rb
|
|
54
54
|
- lib/job-iteration/nested_enumerator.rb
|
|
55
|
+
- lib/job-iteration/parallel_enumerator.rb
|
|
55
56
|
- lib/job-iteration/railtie.rb
|
|
56
57
|
- lib/job-iteration/test_helper.rb
|
|
57
58
|
- lib/job-iteration/throttle_enumerator.rb
|
|
@@ -77,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
78
|
- !ruby/object:Gem::Version
|
|
78
79
|
version: '0'
|
|
79
80
|
requirements: []
|
|
80
|
-
rubygems_version: 4.0.
|
|
81
|
+
rubygems_version: 4.0.11
|
|
81
82
|
specification_version: 4
|
|
82
83
|
summary: Makes your background jobs interruptible and resumable.
|
|
83
84
|
test_files: []
|