job-iteration 1.1.9 → 1.1.14
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.
Potentially problematic release.
This version of job-iteration might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +47 -0
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +27 -1
- data/Gemfile +14 -13
- data/README.md +22 -1
- data/gemfiles/rails_5_2.gemfile +3 -3
- data/gemfiles/rails_6_0.gemfile +3 -3
- data/gemfiles/rails_edge.gemfile +3 -3
- data/job-iteration.gemspec +1 -1
- data/lib/job-iteration.rb +2 -2
- data/lib/job-iteration/active_record_batch_enumerator.rb +117 -0
- data/lib/job-iteration/active_record_cursor.rb +6 -4
- data/lib/job-iteration/active_record_enumerator.rb +1 -1
- data/lib/job-iteration/enumerator_builder.rb +18 -1
- data/lib/job-iteration/integrations/resque.rb +1 -1
- data/lib/job-iteration/integrations/sidekiq.rb +1 -1
- data/lib/job-iteration/iteration.rb +62 -9
- data/lib/job-iteration/version.rb +1 -1
- metadata +5 -4
- data/.travis.yml +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0da9051861fb27696febf2d0191b4d2f459ece64b34cf0231398984c0f36ef3d
|
4
|
+
data.tar.gz: 5fc84c784cdc1a0558a96891a236ac3cea315068e32a6730d633427ef584204b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f30dbb37ada6149fdd877c4b92fff5cf91e078202408230cbdd19593a341c25f4311b4d12dcdceb539a1553e30e0386930a0b49e4c082a4261a0e788c82125b6
|
7
|
+
data.tar.gz: 88e9846617f7c45959b45328e46b1465b5acb43caf1a5e0b9fa4a5d0848d00d842392ef4383e372f68c7ccb7aa3f0954549a64846cd3e15c712d9a0d7a60d781
|
@@ -0,0 +1,47 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
name: Ruby ${{ matrix.ruby }} | Gemfile ${{ matrix.gemfile }}
|
9
|
+
services:
|
10
|
+
redis:
|
11
|
+
image: redis
|
12
|
+
ports:
|
13
|
+
- 6379:6379
|
14
|
+
strategy:
|
15
|
+
matrix:
|
16
|
+
ruby: [2.5, 2.6, 2.7, 3.0]
|
17
|
+
gemfile: [rails_5_2, rails_6_0, rails_edge]
|
18
|
+
exclude:
|
19
|
+
- ruby: 2.5
|
20
|
+
gemfile: rails_edge
|
21
|
+
- ruby: 2.6
|
22
|
+
gemfile: rails_edge
|
23
|
+
- ruby: 3.0
|
24
|
+
gemfile: rails_5_2
|
25
|
+
env:
|
26
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
27
|
+
steps:
|
28
|
+
- name: Check out code
|
29
|
+
uses: actions/checkout@v2
|
30
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
31
|
+
uses: ruby/setup-ruby@v1
|
32
|
+
with:
|
33
|
+
ruby-version: ${{ matrix.ruby }}
|
34
|
+
bundler-cache: true
|
35
|
+
- name: Start MySQL and create DB
|
36
|
+
run: |
|
37
|
+
sudo systemctl start mysql.service
|
38
|
+
mysql -uroot -h localhost -proot -e "CREATE DATABASE job_iteration_test;"
|
39
|
+
- name: Rubocop
|
40
|
+
run: bundle exec rubocop
|
41
|
+
- name: Ruby tests
|
42
|
+
run: bundle exec rake test
|
43
|
+
env:
|
44
|
+
REDIS_HOST: localhost
|
45
|
+
REDIS_PORT: ${{ job.services.redis.ports[6379] }}
|
46
|
+
- name: Documentation correctly written
|
47
|
+
run: bundle exec yardoc --no-output --no-save --no-stats --fail-on-warning
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,12 +1,38 @@
|
|
1
1
|
### Master (unreleased)
|
2
2
|
|
3
|
+
## v1.1.14 (May 28, 2021)
|
4
|
+
|
5
|
+
#### Bug fix
|
6
|
+
- [84](https://github.com/Shopify/job-iteration/pull/84) - Call adjust_total_time before running on_complete callbacks
|
7
|
+
- [94](https://github.com/Shopify/job-iteration/pull/94) - Remove unnecessary break
|
8
|
+
- [95](https://github.com/Shopify/job-iteration/pull/95) - ActiveRecordBatchEnumerator#each should rewind at the end
|
9
|
+
- [97](https://github.com/Shopify/job-iteration/pull/97) - Batch enumerator size returns the number of batches, not records
|
10
|
+
|
11
|
+
## v1.1.13 (May 20, 2021)
|
12
|
+
|
3
13
|
#### New feature
|
14
|
+
- [91](https://github.com/Shopify/job-iteration/pull/91) - Add enumerator yielding batches as Active Record Relations
|
15
|
+
|
16
|
+
## v1.1.12 (April 19, 2021)
|
17
|
+
|
18
|
+
#### Bug fix
|
19
|
+
|
20
|
+
- [77](https://github.com/Shopify/job-iteration/pull/77) - Defer enforce cursor be serializable until 2.0.0
|
21
|
+
|
22
|
+
## v1.1.11 (April 19, 2021)
|
4
23
|
|
5
24
|
#### Bug fix
|
6
25
|
|
26
|
+
- [73](https://github.com/Shopify/job-iteration/pull/73) - Enforce cursor be serializable
|
27
|
+
_This is reverted in 1.1.12 as it breaks behaviour in some apps._
|
28
|
+
|
29
|
+
## v1.1.10 (March 30, 2021)
|
30
|
+
|
31
|
+
- [69](https://github.com/Shopify/job-iteration/pull/69) - Fix memory leak in ActiveRecordCursor
|
32
|
+
|
7
33
|
## v1.1.9 (January 6, 2021)
|
8
34
|
|
9
|
-
- [61](https://github.com/Shopify/job-iteration/pull/61) Call `super` in `method_added`
|
35
|
+
- [61](https://github.com/Shopify/job-iteration/pull/61) - Call `super` in `method_added`
|
10
36
|
|
11
37
|
## v1.1.8 (June 8, 2020)
|
12
38
|
|
data/Gemfile
CHANGED
@@ -8,21 +8,22 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
8
8
|
gemspec
|
9
9
|
|
10
10
|
# for integration testing
|
11
|
-
gem
|
12
|
-
gem
|
11
|
+
gem "sidekiq"
|
12
|
+
gem "resque"
|
13
13
|
|
14
|
-
gem
|
15
|
-
gem
|
16
|
-
gem
|
17
|
-
gem
|
18
|
-
gem
|
14
|
+
gem "mysql2", "~> 0.5"
|
15
|
+
gem "globalid"
|
16
|
+
gem "i18n"
|
17
|
+
gem "redis"
|
18
|
+
gem "database_cleaner"
|
19
19
|
|
20
|
-
gem
|
21
|
-
gem
|
20
|
+
gem "pry"
|
21
|
+
gem "mocha"
|
22
22
|
|
23
|
-
gem
|
24
|
-
gem
|
25
|
-
gem
|
23
|
+
gem "rubocop-shopify", require: false
|
24
|
+
gem "rubocop", "<= 1.12.1", require: false # 1.13.0 drops Ruby 2.4 support
|
25
|
+
gem "yard"
|
26
|
+
gem "rake"
|
26
27
|
|
27
28
|
# for unit testing optional sorbet support
|
28
|
-
gem
|
29
|
+
gem "sorbet-runtime"
|
data/README.md
CHANGED
@@ -77,7 +77,28 @@ class BatchesJob < ApplicationJob
|
|
77
77
|
|
78
78
|
def each_iteration(batch_of_comments, product_id)
|
79
79
|
# batch_of_comments will contain batches of 100 records
|
80
|
-
|
80
|
+
batch_of_comments.each do |comment|
|
81
|
+
DeleteCommentJob.perform_later(comment)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class BatchesAsRelationJob < ApplicationJob
|
89
|
+
include JobIteration::Iteration
|
90
|
+
|
91
|
+
def build_enumerator(product_id, cursor:)
|
92
|
+
enumerator_builder.active_record_on_batch_relations(
|
93
|
+
Product.find(product_id).comments,
|
94
|
+
cursor: cursor,
|
95
|
+
batch_size: 100,
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def each_iteration(batch_of_comments, product_id)
|
100
|
+
# batch_of_comments will be a Comment::ActiveRecord_Relation
|
101
|
+
batch_of_comments.update_all(deleted: true)
|
81
102
|
end
|
82
103
|
end
|
83
104
|
```
|
data/gemfiles/rails_5_2.gemfile
CHANGED
data/gemfiles/rails_6_0.gemfile
CHANGED
data/gemfiles/rails_edge.gemfile
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
eval_gemfile
|
3
|
+
eval_gemfile "../Gemfile"
|
4
4
|
|
5
|
-
gem
|
6
|
-
gem
|
5
|
+
gem "activejob", github: "rails/rails", branch: "main"
|
6
|
+
gem "activerecord", github: "rails/rails", branch: "main"
|
data/job-iteration.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = %w(Shopify)
|
11
11
|
spec.email = ["ops-accounts+shipit@shopify.com"]
|
12
12
|
|
13
|
-
spec.summary =
|
13
|
+
spec.summary = "Makes your background jobs interruptible and resumable."
|
14
14
|
spec.description = spec.summary
|
15
15
|
spec.homepage = "https://github.com/shopify/job-iteration"
|
16
16
|
spec.license = "MIT"
|
data/lib/job-iteration.rb
CHANGED
@@ -54,11 +54,11 @@ module JobIteration
|
|
54
54
|
def load_integration(integration)
|
55
55
|
unless INTEGRATIONS.include?(integration)
|
56
56
|
raise IntegrationLoadError,
|
57
|
-
"#{integration} integration is not supported. Available integrations: #{INTEGRATIONS.join(
|
57
|
+
"#{integration} integration is not supported. Available integrations: #{INTEGRATIONS.join(", ")}"
|
58
58
|
end
|
59
59
|
|
60
60
|
require_relative "./job-iteration/integrations/#{integration}"
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
JobIteration.load_integrations unless ENV[
|
64
|
+
JobIteration.load_integrations unless ENV["ITERATION_DISABLE_AUTOCONFIGURE"]
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JobIteration
|
4
|
+
# Builds Batch Enumerator based on ActiveRecord Relation.
|
5
|
+
# @see EnumeratorBuilder
|
6
|
+
class ActiveRecordBatchEnumerator
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
SQL_DATETIME_WITH_NSEC = "%Y-%m-%d %H:%M:%S.%N"
|
10
|
+
|
11
|
+
def initialize(relation, columns: nil, batch_size: 100, cursor: nil)
|
12
|
+
@batch_size = batch_size
|
13
|
+
@primary_key = "#{relation.table_name}.#{relation.primary_key}"
|
14
|
+
@columns = Array(columns&.map(&:to_s) || @primary_key)
|
15
|
+
@primary_key_index = @columns.index(@primary_key) || @columns.index(relation.primary_key)
|
16
|
+
@pluck_columns = if @primary_key_index
|
17
|
+
@columns
|
18
|
+
else
|
19
|
+
@columns.dup << @primary_key
|
20
|
+
end
|
21
|
+
@cursor = Array.wrap(cursor)
|
22
|
+
@initial_cursor = @cursor
|
23
|
+
raise ArgumentError, "Must specify at least one column" if @columns.empty?
|
24
|
+
if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?(".") }
|
25
|
+
raise ArgumentError, "You need to specify fully-qualified columns if you join a table"
|
26
|
+
end
|
27
|
+
|
28
|
+
if relation.arel.orders.present? || relation.arel.taken.present?
|
29
|
+
raise ConditionNotSupportedError
|
30
|
+
end
|
31
|
+
|
32
|
+
@base_relation = relation.reorder(@columns.join(","))
|
33
|
+
end
|
34
|
+
|
35
|
+
def each
|
36
|
+
return to_enum { size } unless block_given?
|
37
|
+
while (relation = next_batch)
|
38
|
+
yield relation, cursor_value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def size
|
43
|
+
(@base_relation.count + @batch_size - 1) / @batch_size # ceiling division
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def next_batch
|
49
|
+
relation = @base_relation.limit(@batch_size)
|
50
|
+
if conditions.any?
|
51
|
+
relation = relation.where(*conditions)
|
52
|
+
end
|
53
|
+
|
54
|
+
cursor_values, ids = relation.uncached do
|
55
|
+
pluck_columns(relation)
|
56
|
+
end
|
57
|
+
|
58
|
+
cursor = cursor_values.last
|
59
|
+
unless cursor.present?
|
60
|
+
@cursor = @initial_cursor
|
61
|
+
return
|
62
|
+
end
|
63
|
+
# The primary key was plucked, but original cursor did not include it, so we should remove it
|
64
|
+
cursor.pop unless @primary_key_index
|
65
|
+
@cursor = Array.wrap(cursor)
|
66
|
+
|
67
|
+
# Yields relations by selecting the primary keys of records in the batch.
|
68
|
+
# Post.where(published: nil) results in an enumerator of relations like: Post.where(ids: batch_of_ids)
|
69
|
+
@base_relation.where(@primary_key => ids)
|
70
|
+
end
|
71
|
+
|
72
|
+
def pluck_columns(relation)
|
73
|
+
if @pluck_columns.size == 1 # only the primary key
|
74
|
+
column_values = relation.pluck(*@pluck_columns)
|
75
|
+
return [column_values, column_values]
|
76
|
+
end
|
77
|
+
|
78
|
+
column_values = relation.pluck(*@pluck_columns)
|
79
|
+
primary_key_index = @primary_key_index || -1
|
80
|
+
primary_key_values = column_values.map { |values| values[primary_key_index] }
|
81
|
+
|
82
|
+
serialize_column_values!(column_values)
|
83
|
+
[column_values, primary_key_values]
|
84
|
+
end
|
85
|
+
|
86
|
+
def cursor_value
|
87
|
+
return @cursor.first if @cursor.size == 1
|
88
|
+
@cursor
|
89
|
+
end
|
90
|
+
|
91
|
+
def conditions
|
92
|
+
column_index = @cursor.size - 1
|
93
|
+
column = @columns[column_index]
|
94
|
+
where_clause = if @columns.size == @cursor.size
|
95
|
+
"#{column} > ?"
|
96
|
+
else
|
97
|
+
"#{column} >= ?"
|
98
|
+
end
|
99
|
+
while column_index > 0
|
100
|
+
column_index -= 1
|
101
|
+
column = @columns[column_index]
|
102
|
+
where_clause = "#{column} > ? OR (#{column} = ? AND (#{where_clause}))"
|
103
|
+
end
|
104
|
+
ret = @cursor.reduce([where_clause]) { |params, value| params << value << value }
|
105
|
+
ret.pop
|
106
|
+
ret
|
107
|
+
end
|
108
|
+
|
109
|
+
def serialize_column_values!(column_values)
|
110
|
+
column_values.map! { |values| values.map! { |value| column_value(value) } }
|
111
|
+
end
|
112
|
+
|
113
|
+
def column_value(value)
|
114
|
+
value.is_a?(Time) ? value.strftime(SQL_DATETIME_WITH_NSEC) : value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -23,7 +23,7 @@ module JobIteration
|
|
23
23
|
@columns = Array.wrap(columns)
|
24
24
|
self.position = Array.wrap(position)
|
25
25
|
raise ArgumentError, "Must specify at least one column" if columns.empty?
|
26
|
-
if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?(
|
26
|
+
if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?(".") }
|
27
27
|
raise ArgumentError, "You need to specify fully-qualified columns if you join a table"
|
28
28
|
end
|
29
29
|
|
@@ -31,7 +31,7 @@ module JobIteration
|
|
31
31
|
raise ConditionNotSupportedError
|
32
32
|
end
|
33
33
|
|
34
|
-
@base_relation = relation.reorder(@columns.join(
|
34
|
+
@base_relation = relation.reorder(@columns.join(","))
|
35
35
|
@reached_end = false
|
36
36
|
end
|
37
37
|
|
@@ -50,7 +50,7 @@ module JobIteration
|
|
50
50
|
|
51
51
|
def update_from_record(record)
|
52
52
|
self.position = @columns.map do |column|
|
53
|
-
method = column.to_s.split(
|
53
|
+
method = column.to_s.split(".").last
|
54
54
|
record.send(method.to_sym)
|
55
55
|
end
|
56
56
|
end
|
@@ -64,7 +64,9 @@ module JobIteration
|
|
64
64
|
relation = relation.where(*conditions)
|
65
65
|
end
|
66
66
|
|
67
|
-
records = relation.
|
67
|
+
records = relation.uncached do
|
68
|
+
relation.to_a
|
69
|
+
end
|
68
70
|
|
69
71
|
update_from_record(records.last) unless records.empty?
|
70
72
|
@reached_end = records.size < batch_size
|
@@ -40,7 +40,7 @@ module JobIteration
|
|
40
40
|
|
41
41
|
def cursor_value(record)
|
42
42
|
positions = @columns.map do |column|
|
43
|
-
attribute_name = column.to_s.split(
|
43
|
+
attribute_name = column.to_s.split(".").last
|
44
44
|
column_value(record, attribute_name)
|
45
45
|
end
|
46
46
|
return positions.first if positions.size == 1
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require_relative "./active_record_batch_enumerator"
|
2
3
|
require_relative "./active_record_enumerator"
|
3
4
|
require_relative "./csv_enumerator"
|
4
5
|
require_relative "./throttle_enumerator"
|
@@ -86,6 +87,11 @@ module JobIteration
|
|
86
87
|
# WHERE (created_at > '$LAST_CREATED_AT_CURSOR'
|
87
88
|
# OR (created_at = '$LAST_CREATED_AT_CURSOR' AND (id > '$LAST_ID_CURSOR')))
|
88
89
|
# ORDER BY created_at, id LIMIT 100
|
90
|
+
#
|
91
|
+
# As a result of this query pattern, if the values in these columns change for the records in scope during
|
92
|
+
# iteration, they may be skipped or yielded multiple times depending on the nature of the update and the
|
93
|
+
# cursor's value. If the value gets updated to a greater value than the cursor's value, it will get yielded
|
94
|
+
# again. Similarly, if the value gets updated to a lesser value than the curor's value, it will get skipped.
|
89
95
|
def build_active_record_enumerator_on_records(scope, cursor:, **args)
|
90
96
|
enum = build_active_record_enumerator(
|
91
97
|
scope,
|
@@ -95,7 +101,7 @@ module JobIteration
|
|
95
101
|
wrap(self, enum)
|
96
102
|
end
|
97
103
|
|
98
|
-
# Builds Enumerator from Active Record Relation and enumerates on batches.
|
104
|
+
# Builds Enumerator from Active Record Relation and enumerates on batches of records.
|
99
105
|
# Each Enumerator tick moves the cursor +batch_size+ rows forward.
|
100
106
|
#
|
101
107
|
# +batch_size:+ sets how many records will be fetched in one batch. Defaults to 100.
|
@@ -110,6 +116,16 @@ module JobIteration
|
|
110
116
|
wrap(self, enum)
|
111
117
|
end
|
112
118
|
|
119
|
+
# Builds Enumerator from Active Record Relation and enumerates on batches, yielding Active Record Relations.
|
120
|
+
# See documentation for #build_active_record_enumerator_on_batches.
|
121
|
+
def build_active_record_enumerator_on_batch_relations(scope, cursor:, **args)
|
122
|
+
JobIteration::ActiveRecordBatchEnumerator.new(
|
123
|
+
scope,
|
124
|
+
cursor: cursor,
|
125
|
+
**args
|
126
|
+
).each
|
127
|
+
end
|
128
|
+
|
113
129
|
def build_throttle_enumerator(enum, throttle_on:, backoff:)
|
114
130
|
JobIteration::ThrottleEnumerator.new(
|
115
131
|
enum,
|
@@ -124,6 +140,7 @@ module JobIteration
|
|
124
140
|
alias_method :array, :build_array_enumerator
|
125
141
|
alias_method :active_record_on_records, :build_active_record_enumerator_on_records
|
126
142
|
alias_method :active_record_on_batches, :build_active_record_enumerator_on_batches
|
143
|
+
alias_method :active_record_on_batch_relations, :build_active_record_enumerator_on_batch_relations
|
127
144
|
alias_method :throttle, :build_throttle_enumerator
|
128
145
|
|
129
146
|
private
|
@@ -1,11 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "active_support/all"
|
4
4
|
|
5
5
|
module JobIteration
|
6
6
|
module Iteration
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
|
+
class CursorError < ArgumentError
|
10
|
+
attr_reader :cursor
|
11
|
+
|
12
|
+
def initialize(message, cursor:)
|
13
|
+
super(message)
|
14
|
+
@cursor = cursor
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
"#{super} (#{inspected_cursor})"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def inspected_cursor
|
24
|
+
cursor.inspect
|
25
|
+
rescue NoMethodError
|
26
|
+
# For those brave enough to try to use BasicObject as cursor. Nice try.
|
27
|
+
Object.instance_method(:inspect).bind(cursor).call
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
9
31
|
included do |_base|
|
10
32
|
attr_accessor(
|
11
33
|
:cursor_position,
|
@@ -54,17 +76,17 @@ module JobIteration
|
|
54
76
|
|
55
77
|
def serialize # @private
|
56
78
|
super.merge(
|
57
|
-
|
58
|
-
|
59
|
-
|
79
|
+
"cursor_position" => cursor_position,
|
80
|
+
"times_interrupted" => times_interrupted,
|
81
|
+
"total_time" => total_time,
|
60
82
|
)
|
61
83
|
end
|
62
84
|
|
63
85
|
def deserialize(job_data) # @private
|
64
86
|
super
|
65
|
-
self.cursor_position = job_data[
|
66
|
-
self.times_interrupted = job_data[
|
67
|
-
self.total_time = job_data[
|
87
|
+
self.cursor_position = job_data["cursor_position"]
|
88
|
+
self.times_interrupted = job_data["times_interrupted"] || 0
|
89
|
+
self.total_time = job_data["total_time"] || 0
|
68
90
|
end
|
69
91
|
|
70
92
|
def perform(*params) # @private
|
@@ -120,6 +142,9 @@ module JobIteration
|
|
120
142
|
arguments = arguments.dup.freeze
|
121
143
|
found_record = false
|
122
144
|
enumerator.each do |object_from_enumerator, index|
|
145
|
+
# Deferred until 2.0.0
|
146
|
+
# assert_valid_cursor!(index)
|
147
|
+
|
123
148
|
record_unit_of_work do
|
124
149
|
found_record = true
|
125
150
|
each_iteration(object_from_enumerator, *arguments)
|
@@ -137,6 +162,8 @@ module JobIteration
|
|
137
162
|
"times_interrupted=#{times_interrupted} cursor_position=#{cursor_position}"
|
138
163
|
) unless found_record
|
139
164
|
|
165
|
+
adjust_total_time
|
166
|
+
|
140
167
|
true
|
141
168
|
end
|
142
169
|
|
@@ -176,6 +203,18 @@ module JobIteration
|
|
176
203
|
EOS
|
177
204
|
end
|
178
205
|
|
206
|
+
# The adapter must be able to serialize and deserialize the cursor back into an equivalent object.
|
207
|
+
# https://github.com/mperham/sidekiq/wiki/Best-Practices#1-make-your-job-parameters-small-and-simple
|
208
|
+
def assert_valid_cursor!(cursor)
|
209
|
+
return if serializable?(cursor)
|
210
|
+
|
211
|
+
raise CursorError.new(
|
212
|
+
"Cursor must be composed of objects capable of built-in (de)serialization: " \
|
213
|
+
"Strings, Integers, Floats, Arrays, Hashes, true, false, or nil.",
|
214
|
+
cursor: cursor,
|
215
|
+
)
|
216
|
+
end
|
217
|
+
|
179
218
|
def assert_implements_methods!
|
180
219
|
unless respond_to?(:each_iteration, true)
|
181
220
|
raise(
|
@@ -212,8 +251,6 @@ module JobIteration
|
|
212
251
|
end
|
213
252
|
|
214
253
|
def output_interrupt_summary
|
215
|
-
adjust_total_time
|
216
|
-
|
217
254
|
message = "[JobIteration::Iteration] Completed iterating. times_interrupted=%d total_time=%.3f"
|
218
255
|
logger.info(Kernel.format(message, times_interrupted, total_time))
|
219
256
|
end
|
@@ -251,5 +288,21 @@ module JobIteration
|
|
251
288
|
end
|
252
289
|
false
|
253
290
|
end
|
291
|
+
|
292
|
+
SIMPLE_SERIALIZABLE_CLASSES = [String, Integer, Float, NilClass, TrueClass, FalseClass].freeze
|
293
|
+
private_constant :SIMPLE_SERIALIZABLE_CLASSES
|
294
|
+
def serializable?(object)
|
295
|
+
# Subclasses must be excluded, hence not using is_a? or ===.
|
296
|
+
if object.instance_of?(Array)
|
297
|
+
object.all? { |element| serializable?(element) }
|
298
|
+
elsif object.instance_of?(Hash)
|
299
|
+
object.all? { |key, value| serializable?(key) && serializable?(value) }
|
300
|
+
else
|
301
|
+
SIMPLE_SERIALIZABLE_CLASSES.any? { |klass| object.instance_of?(klass) }
|
302
|
+
end
|
303
|
+
rescue NoMethodError
|
304
|
+
# BasicObject doesn't respond to instance_of, but we can't serialize it anyway
|
305
|
+
false
|
306
|
+
end
|
254
307
|
end
|
255
308
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: job-iteration
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -45,9 +45,9 @@ executables: []
|
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
+
- ".github/workflows/ci.yml"
|
48
49
|
- ".gitignore"
|
49
50
|
- ".rubocop.yml"
|
50
|
-
- ".travis.yml"
|
51
51
|
- ".yardopts"
|
52
52
|
- CHANGELOG.md
|
53
53
|
- CODE_OF_CONDUCT.md
|
@@ -66,6 +66,7 @@ files:
|
|
66
66
|
- guides/throttling.md
|
67
67
|
- job-iteration.gemspec
|
68
68
|
- lib/job-iteration.rb
|
69
|
+
- lib/job-iteration/active_record_batch_enumerator.rb
|
69
70
|
- lib/job-iteration/active_record_cursor.rb
|
70
71
|
- lib/job-iteration/active_record_enumerator.rb
|
71
72
|
- lib/job-iteration/csv_enumerator.rb
|
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
99
|
- !ruby/object:Gem::Version
|
99
100
|
version: '0'
|
100
101
|
requirements: []
|
101
|
-
rubygems_version: 3.
|
102
|
+
rubygems_version: 3.2.17
|
102
103
|
signing_key:
|
103
104
|
specification_version: 4
|
104
105
|
summary: Makes your background jobs interruptible and resumable.
|
data/.travis.yml
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
services:
|
2
|
-
- mysql
|
3
|
-
- redis-server
|
4
|
-
language: ruby
|
5
|
-
rvm:
|
6
|
-
- 2.5
|
7
|
-
- 2.6
|
8
|
-
- 2.7
|
9
|
-
before_install:
|
10
|
-
- mysql -e 'CREATE DATABASE job_iteration_test;'
|
11
|
-
script:
|
12
|
-
- bundle exec rake test
|
13
|
-
- bundle exec rubocop
|
14
|
-
- bundle exec yardoc --no-output --no-save --no-stats --fail-on-warning
|
15
|
-
|
16
|
-
gemfile:
|
17
|
-
- 'gemfiles/rails_5_2.gemfile'
|
18
|
-
- 'gemfiles/rails_6_0.gemfile'
|
19
|
-
- 'gemfiles/rails_edge.gemfile'
|