job-iteration 1.9.0 → 1.11.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 +28 -1
- data/README.md +19 -1
- data/job-iteration.gemspec +4 -4
- data/lib/job-iteration/csv_enumerator.rb +6 -10
- data/lib/job-iteration/interruption_adapters/delayed_job_adapter.rb +54 -0
- data/lib/job-iteration/interruption_adapters.rb +1 -1
- data/lib/job-iteration/version.rb +1 -1
- data/lib/tapioca/dsl/compilers/job_iteration.rb +15 -6
- metadata +7 -26
- data/.github/dependabot.yml +0 -16
- data/.github/workflows/ci.yml +0 -98
- data/.github/workflows/cla.yml +0 -22
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -16
- data/.ruby-version +0 -1
- data/.yardopts +0 -3
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -42
- data/Gemfile.lock +0 -192
- data/Rakefile +0 -12
- data/bin/setup +0 -23
- data/bin/test +0 -32
- data/dev.yml +0 -54
- data/gemfiles/rails_gems.gemfile +0 -18
- data/guides/argument-semantics.md +0 -128
- data/guides/best-practices.md +0 -108
- data/guides/custom-enumerator.md +0 -140
- data/guides/iteration-how-it-works.md +0 -51
- data/guides/throttling.md +0 -68
data/Gemfile.lock
DELETED
@@ -1,192 +0,0 @@
|
|
1
|
-
GIT
|
2
|
-
remote: https://github.com/brianmario/mysql2
|
3
|
-
revision: 57b8df188c963ae0e4d4e1123d3e9de2bbcab637
|
4
|
-
specs:
|
5
|
-
mysql2 (0.5.6)
|
6
|
-
bigdecimal
|
7
|
-
|
8
|
-
PATH
|
9
|
-
remote: .
|
10
|
-
specs:
|
11
|
-
job-iteration (1.9.0)
|
12
|
-
activejob (>= 5.2)
|
13
|
-
|
14
|
-
GEM
|
15
|
-
remote: https://rubygems.org/
|
16
|
-
specs:
|
17
|
-
activejob (8.0.1)
|
18
|
-
activesupport (= 8.0.1)
|
19
|
-
globalid (>= 0.3.6)
|
20
|
-
activemodel (8.0.1)
|
21
|
-
activesupport (= 8.0.1)
|
22
|
-
activerecord (8.0.1)
|
23
|
-
activemodel (= 8.0.1)
|
24
|
-
activesupport (= 8.0.1)
|
25
|
-
timeout (>= 0.4.0)
|
26
|
-
activesupport (8.0.1)
|
27
|
-
base64
|
28
|
-
benchmark (>= 0.3)
|
29
|
-
bigdecimal
|
30
|
-
concurrent-ruby (~> 1.0, >= 1.3.1)
|
31
|
-
connection_pool (>= 2.2.5)
|
32
|
-
drb
|
33
|
-
i18n (>= 1.6, < 2)
|
34
|
-
logger (>= 1.4.2)
|
35
|
-
minitest (>= 5.1)
|
36
|
-
securerandom (>= 0.3)
|
37
|
-
tzinfo (~> 2.0, >= 2.0.5)
|
38
|
-
uri (>= 0.13.1)
|
39
|
-
ast (2.4.2)
|
40
|
-
base64 (0.2.0)
|
41
|
-
benchmark (0.4.0)
|
42
|
-
bigdecimal (3.1.9)
|
43
|
-
coderay (1.1.3)
|
44
|
-
concurrent-ruby (1.3.5)
|
45
|
-
connection_pool (2.5.0)
|
46
|
-
csv (3.3.2)
|
47
|
-
drb (2.2.1)
|
48
|
-
erubi (1.13.1)
|
49
|
-
globalid (1.2.1)
|
50
|
-
activesupport (>= 6.1)
|
51
|
-
i18n (1.14.7)
|
52
|
-
concurrent-ruby (~> 1.0)
|
53
|
-
json (2.9.1)
|
54
|
-
language_server-protocol (3.17.0.4)
|
55
|
-
logger (1.6.5)
|
56
|
-
method_source (1.1.0)
|
57
|
-
minitest (5.25.4)
|
58
|
-
mocha (2.7.1)
|
59
|
-
ruby2_keywords (>= 0.0.5)
|
60
|
-
mono_logger (1.1.2)
|
61
|
-
multi_json (1.15.0)
|
62
|
-
mustermann (3.0.3)
|
63
|
-
ruby2_keywords (~> 0.0.1)
|
64
|
-
netrc (0.11.0)
|
65
|
-
parallel (1.26.3)
|
66
|
-
parser (3.3.7.0)
|
67
|
-
ast (~> 2.4.1)
|
68
|
-
racc
|
69
|
-
prism (1.3.0)
|
70
|
-
pry (0.15.2)
|
71
|
-
coderay (~> 1.1)
|
72
|
-
method_source (~> 1.0)
|
73
|
-
racc (1.8.1)
|
74
|
-
rack (3.1.8)
|
75
|
-
rack-protection (4.1.1)
|
76
|
-
base64 (>= 0.1.0)
|
77
|
-
logger (>= 1.6.0)
|
78
|
-
rack (>= 3.0.0, < 4)
|
79
|
-
rack-session (2.1.0)
|
80
|
-
base64 (>= 0.1.0)
|
81
|
-
rack (>= 3.0.0)
|
82
|
-
rainbow (3.1.1)
|
83
|
-
rake (13.2.1)
|
84
|
-
rbi (0.2.4)
|
85
|
-
prism (~> 1.0)
|
86
|
-
sorbet-runtime (>= 0.5.9204)
|
87
|
-
redis (5.3.0)
|
88
|
-
redis-client (>= 0.22.0)
|
89
|
-
redis-client (0.23.2)
|
90
|
-
connection_pool
|
91
|
-
redis-namespace (1.11.0)
|
92
|
-
redis (>= 4)
|
93
|
-
regexp_parser (2.10.0)
|
94
|
-
resque (2.7.0)
|
95
|
-
mono_logger (~> 1)
|
96
|
-
multi_json (~> 1.0)
|
97
|
-
redis-namespace (~> 1.6)
|
98
|
-
sinatra (>= 0.9.2)
|
99
|
-
rubocop (1.71.0)
|
100
|
-
json (~> 2.3)
|
101
|
-
language_server-protocol (>= 3.17.0)
|
102
|
-
parallel (~> 1.10)
|
103
|
-
parser (>= 3.3.0.2)
|
104
|
-
rainbow (>= 2.2.2, < 4.0)
|
105
|
-
regexp_parser (>= 2.9.3, < 3.0)
|
106
|
-
rubocop-ast (>= 1.36.2, < 2.0)
|
107
|
-
ruby-progressbar (~> 1.7)
|
108
|
-
unicode-display_width (>= 2.4.0, < 4.0)
|
109
|
-
rubocop-ast (1.38.0)
|
110
|
-
parser (>= 3.3.1.0)
|
111
|
-
rubocop-shopify (2.15.1)
|
112
|
-
rubocop (~> 1.51)
|
113
|
-
ruby-progressbar (1.13.0)
|
114
|
-
ruby2_keywords (0.0.5)
|
115
|
-
securerandom (0.4.1)
|
116
|
-
sidekiq (7.3.8)
|
117
|
-
base64
|
118
|
-
connection_pool (>= 2.3.0)
|
119
|
-
logger
|
120
|
-
rack (>= 2.2.4)
|
121
|
-
redis-client (>= 0.22.2)
|
122
|
-
sinatra (4.1.1)
|
123
|
-
logger (>= 1.6.0)
|
124
|
-
mustermann (~> 3.0)
|
125
|
-
rack (>= 3.0.0, < 4)
|
126
|
-
rack-protection (= 4.1.1)
|
127
|
-
rack-session (>= 2.0.0, < 3)
|
128
|
-
tilt (~> 2.0)
|
129
|
-
sorbet (0.5.11787)
|
130
|
-
sorbet-static (= 0.5.11787)
|
131
|
-
sorbet-runtime (0.5.11787)
|
132
|
-
sorbet-static (0.5.11787-universal-darwin)
|
133
|
-
sorbet-static (0.5.11787-x86_64-linux)
|
134
|
-
sorbet-static-and-runtime (0.5.11787)
|
135
|
-
sorbet (= 0.5.11787)
|
136
|
-
sorbet-runtime (= 0.5.11787)
|
137
|
-
spoom (1.5.2)
|
138
|
-
erubi (>= 1.10.0)
|
139
|
-
prism (>= 0.28.0)
|
140
|
-
rbi (>= 0.2.3)
|
141
|
-
sorbet-static-and-runtime (>= 0.5.10187)
|
142
|
-
thor (>= 0.19.2)
|
143
|
-
tapioca (0.16.8)
|
144
|
-
benchmark
|
145
|
-
bundler (>= 2.2.25)
|
146
|
-
netrc (>= 0.11.0)
|
147
|
-
parallel (>= 1.21.0)
|
148
|
-
rbi (~> 0.2)
|
149
|
-
sorbet-static-and-runtime (>= 0.5.11087)
|
150
|
-
spoom (>= 1.2.0)
|
151
|
-
thor (>= 1.2.0)
|
152
|
-
yard-sorbet
|
153
|
-
thor (1.3.2)
|
154
|
-
tilt (2.6.0)
|
155
|
-
timeout (0.4.3)
|
156
|
-
tzinfo (2.0.6)
|
157
|
-
concurrent-ruby (~> 1.0)
|
158
|
-
unicode-display_width (3.1.4)
|
159
|
-
unicode-emoji (~> 4.0, >= 4.0.4)
|
160
|
-
unicode-emoji (4.0.4)
|
161
|
-
uri (1.0.2)
|
162
|
-
yard (0.9.37)
|
163
|
-
yard-sorbet (0.9.0)
|
164
|
-
sorbet-runtime
|
165
|
-
yard
|
166
|
-
|
167
|
-
PLATFORMS
|
168
|
-
arm64-darwin
|
169
|
-
x86_64-darwin
|
170
|
-
x86_64-linux
|
171
|
-
|
172
|
-
DEPENDENCIES
|
173
|
-
activerecord
|
174
|
-
csv
|
175
|
-
globalid
|
176
|
-
i18n
|
177
|
-
job-iteration!
|
178
|
-
logger
|
179
|
-
mocha
|
180
|
-
mysql2!
|
181
|
-
pry
|
182
|
-
rake
|
183
|
-
redis
|
184
|
-
resque
|
185
|
-
rubocop-shopify
|
186
|
-
sidekiq
|
187
|
-
sorbet-runtime
|
188
|
-
tapioca
|
189
|
-
yard
|
190
|
-
|
191
|
-
BUNDLED WITH
|
192
|
-
2.6.1
|
data/Rakefile
DELETED
data/bin/setup
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
if ! [ -x "$(command -v mysql)" ];
|
4
|
-
then
|
5
|
-
echo "Error: mysql is not installed." >&2
|
6
|
-
echo "You need to install mysql"
|
7
|
-
exit 1
|
8
|
-
else
|
9
|
-
echo "Installing dependencies"
|
10
|
-
bundle install --quiet
|
11
|
-
|
12
|
-
mysql.server start > /dev/null 2>&1
|
13
|
-
mysql -uroot job_iteration_test -e exit > /dev/null 2>&1
|
14
|
-
|
15
|
-
if [ $? -eq 0 ];
|
16
|
-
then
|
17
|
-
echo "Setup completed!"
|
18
|
-
else
|
19
|
-
echo "Creating job_iteration_test database"
|
20
|
-
mysql -uroot -e "CREATE DATABASE job_iteration_test" > /dev/null 2>&1
|
21
|
-
echo "Setup completed!"
|
22
|
-
fi
|
23
|
-
fi
|
data/bin/test
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
def main
|
5
|
-
begin
|
6
|
-
command = create_command
|
7
|
-
rescue ArgumentError => e
|
8
|
-
abort(e.message)
|
9
|
-
end
|
10
|
-
puts "Running #{command.join(" ")}"
|
11
|
-
system(*command)
|
12
|
-
end
|
13
|
-
|
14
|
-
def create_command
|
15
|
-
case ARGV.length
|
16
|
-
when 0
|
17
|
-
["bundle", "exec", "rake", "test"]
|
18
|
-
when 1
|
19
|
-
filename = ARGV[0]
|
20
|
-
["bundle", "exec", "rake", "test", "TEST=#{filename}"]
|
21
|
-
when 2
|
22
|
-
filename = ARGV[0]
|
23
|
-
test_name = ARGV[1]
|
24
|
-
test_name_with_underscores = test_name.tr(" ", "_")
|
25
|
-
test_name_pattern = "/#{Regexp.escape(test_name_with_underscores)}/"
|
26
|
-
["bundle", "exec", "rake", "test", "TEST=#{filename}", "TESTOPTS=\"--name=#{test_name_pattern} -v\""]
|
27
|
-
else
|
28
|
-
raise ArgumentError, "Too many arguments. Did you forget to put the test name in quotes?"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
main
|
data/dev.yml
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
# This file is for Shopify employees development environment.
|
2
|
-
# If you are an external contributor you don't have to bother with it.
|
3
|
-
name: job-iteration
|
4
|
-
|
5
|
-
up:
|
6
|
-
- packages:
|
7
|
-
- mysql_client
|
8
|
-
- ruby
|
9
|
-
- bundler
|
10
|
-
- mysql
|
11
|
-
- redis
|
12
|
-
- custom:
|
13
|
-
name: Create Job Iteration database
|
14
|
-
meet: mysql -uroot -h $MYSQL_HOST -P $MYSQL_PORT -e "CREATE DATABASE job_iteration_test"
|
15
|
-
met?: mysql -uroot -h $MYSQL_HOST -P $MYSQL_PORT job_iteration_test -e "SELECT 1" &> /dev/null
|
16
|
-
|
17
|
-
commands:
|
18
|
-
test:
|
19
|
-
run: bin/test "$@"
|
20
|
-
syntax:
|
21
|
-
optional: filename testnamepattern
|
22
|
-
aliases: [t]
|
23
|
-
desc: run tests
|
24
|
-
long_desc: |
|
25
|
-
{{bold:Default}}
|
26
|
-
=======
|
27
|
-
Run the entire test suite.
|
28
|
-
|
29
|
-
Examples:
|
30
|
-
{{command:dev test}}
|
31
|
-
{{command:dev t}}
|
32
|
-
|
33
|
-
{{bold:Run all tests in a file}}
|
34
|
-
========================
|
35
|
-
Include the file path.
|
36
|
-
|
37
|
-
Example:
|
38
|
-
{{command:dev test test/unit/iteration_test.rb}}
|
39
|
-
|
40
|
-
{{bold:Run a single test in a given file}}
|
41
|
-
========================
|
42
|
-
Include the file path and the name of the test you'd like to run.
|
43
|
-
|
44
|
-
Example:
|
45
|
-
{{command:dev test test/unit/iteration_test.rb test_that_it_has_a_version_number}}
|
46
|
-
|
47
|
-
{{bold:Run all tests in a given file whose name contains a string}}
|
48
|
-
========================
|
49
|
-
Include the file path and the string that the test names should contain.
|
50
|
-
|
51
|
-
Example:
|
52
|
-
{{command:dev test test/unit/iteration_test.rb version_number}}
|
53
|
-
style:
|
54
|
-
run: bundle exec rubocop -a
|
data/gemfiles/rails_gems.gemfile
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
rails_version = ENV.fetch("RAILS_VERSION")
|
4
|
-
@rails_gems_requirements = case rails_version
|
5
|
-
when "edge" then { github: "rails/rails", branch: "main" }
|
6
|
-
when /\A\d+\.\d+\z/ then "~> #{rails_version}.0"
|
7
|
-
else raise "Unsupported RAILS_VERSION: #{rails_version}"
|
8
|
-
end
|
9
|
-
|
10
|
-
eval_gemfile "../Gemfile"
|
11
|
-
|
12
|
-
# https://github.com/rails/rails/pull/44083
|
13
|
-
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1") &&
|
14
|
-
rails_version != "edge" && Gem::Version.new(rails_version) < Gem::Version.new("7")
|
15
|
-
gem "net-imap", require: false
|
16
|
-
gem "net-pop", require: false
|
17
|
-
gem "net-smtp", require: false
|
18
|
-
end
|
@@ -1,128 +0,0 @@
|
|
1
|
-
`job-iteration` overrides the `perform` method of `ActiveJob::Base` to allow for iteration. The `perform` method preserves all the standard calling conventions of the original, but the way the subsequent methods work might differ from what one expects from an ActiveJob subclass.
|
2
|
-
|
3
|
-
The call sequence is usually 3 methods:
|
4
|
-
|
5
|
-
`perform -> build_enumerator -> each_iteration|each_batch`
|
6
|
-
|
7
|
-
In that sense `job-iteration` works like a framework (it calls your code) rather than like a library (that you call). When using jobs with parameters, the following rules of thumb are good to keep in mind.
|
8
|
-
|
9
|
-
### Jobs without arguments
|
10
|
-
|
11
|
-
Jobs without arguments do not pass anything into either `build_enumerator` or `each_iteration` except for the `cursor` which `job-iteration` persists by itself:
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
class ArglessJob < ActiveJob::Base
|
15
|
-
include JobIteration::Iteration
|
16
|
-
|
17
|
-
def build_enumerator(cursor:)
|
18
|
-
# ...
|
19
|
-
end
|
20
|
-
|
21
|
-
def each_iteration(single_object_yielded_from_enumerator)
|
22
|
-
# ...
|
23
|
-
end
|
24
|
-
end
|
25
|
-
```
|
26
|
-
|
27
|
-
To enqueue the job:
|
28
|
-
|
29
|
-
```ruby
|
30
|
-
ArglessJob.perform_later
|
31
|
-
```
|
32
|
-
|
33
|
-
### Jobs with positional arguments
|
34
|
-
|
35
|
-
Jobs with positional arguments will have those arguments available to both `build_enumerator` and `each_iteration`:
|
36
|
-
|
37
|
-
```ruby
|
38
|
-
class ArgumentativeJob < ActiveJob::Base
|
39
|
-
include JobIteration::Iteration
|
40
|
-
|
41
|
-
def build_enumerator(arg1, arg2, arg3, cursor:)
|
42
|
-
# ...
|
43
|
-
end
|
44
|
-
|
45
|
-
def each_iteration(single_object_yielded_from_enumerator, arg1, arg2, arg3)
|
46
|
-
# ...
|
47
|
-
end
|
48
|
-
end
|
49
|
-
```
|
50
|
-
|
51
|
-
To enqueue the job:
|
52
|
-
|
53
|
-
```ruby
|
54
|
-
ArgumentativeJob.perform_later(_arg1 = "One", _arg2 = "Two", _arg3 = "Three")
|
55
|
-
```
|
56
|
-
|
57
|
-
### Jobs with keyword arguments
|
58
|
-
|
59
|
-
Jobs with keyword arguments will have the keyword arguments available to both `build_enumerator` and `each_iteration`, but these arguments come packaged into a Hash in both cases. You will need to `fetch` or `[]` your parameter from the `Hash` you get passed in:
|
60
|
-
|
61
|
-
```ruby
|
62
|
-
class ParameterizedJob < ActiveJob::Base
|
63
|
-
include JobIteration::Iteration
|
64
|
-
|
65
|
-
def build_enumerator(kwargs, cursor:)
|
66
|
-
name = kwargs.fetch(:name)
|
67
|
-
email = kwargs.fetch(:email)
|
68
|
-
# ...
|
69
|
-
end
|
70
|
-
|
71
|
-
def each_iteration(object_yielded_from_enumerator, kwargs)
|
72
|
-
name = kwargs.fetch(:name)
|
73
|
-
email = kwargs.fetch(:email)
|
74
|
-
# ...
|
75
|
-
end
|
76
|
-
end
|
77
|
-
```
|
78
|
-
|
79
|
-
To enqueue the job:
|
80
|
-
|
81
|
-
```ruby
|
82
|
-
ParameterizedJob.perform_later(name: "Jane", email: "jane@host.example")
|
83
|
-
```
|
84
|
-
|
85
|
-
Note that you cannot use `ruby2_keywords` at present, and the keyword arguments syntax is not supported in `each_iteration` / `build_enumerator`.
|
86
|
-
|
87
|
-
### Jobs with both positional and keyword arguments
|
88
|
-
|
89
|
-
Jobs with keyword arguments will have the keyword arguments available to both `build_enumerator` and `each_iteration`, but these arguments come packaged into a Hash in both cases. You will need to `fetch` or `[]` your parameter from the `Hash` you get passed in. Positional arguments get passed first and "unsplatted" (not combined into an array), the `Hash` containing keyword arguments comes after:
|
90
|
-
|
91
|
-
```ruby
|
92
|
-
class HighlyConfigurableGreetingJob < ActiveJob::Base
|
93
|
-
include JobIteration::Iteration
|
94
|
-
|
95
|
-
def build_enumerator(subject_line, kwargs, cursor:)
|
96
|
-
name = kwargs.fetch(:sender_name)
|
97
|
-
email = kwargs.fetch(:sender_email)
|
98
|
-
# ...
|
99
|
-
end
|
100
|
-
|
101
|
-
def each_iteration(object_yielded_from_enumerator, subject_line, kwargs)
|
102
|
-
name = kwargs.fetch(:sender_name)
|
103
|
-
email = kwargs.fetch(:sender_email)
|
104
|
-
# ...
|
105
|
-
end
|
106
|
-
end
|
107
|
-
```
|
108
|
-
|
109
|
-
To enqueue the job:
|
110
|
-
|
111
|
-
```ruby
|
112
|
-
HighlyConfigurableGreetingJob.perform_later(_subject_line = "Greetings everybody!", sender_name: "Jane", sender_email: "jane@host.example")
|
113
|
-
```
|
114
|
-
|
115
|
-
Note that you cannot use `ruby2_keywords` at present, and the keyword arguments syntax is not supported in `each_iteration` / `build_enumerator`.
|
116
|
-
|
117
|
-
### Returning (yielding) from enumerators
|
118
|
-
|
119
|
-
When defining a custom enumerator (see the [custom enumerator guide](custom-enumerator.md)) you need to yield two positional arguments from it: the object that will be the value for the current iteration (like a single ActiveModel instance, a single number...) and the value you want to be persisted as the `cursor` value should `job-iteration` decide to interrupt you after this iteration. Calling the enumerator with that cursor should return the next object after the one returned in this iteration. That new `cursor` value does not get passed to `each_iteration`:
|
120
|
-
|
121
|
-
```ruby
|
122
|
-
Enumerator.new do |yielder|
|
123
|
-
# In this case `cursor` is an Integer
|
124
|
-
cursor.upto(99999) do |offset|
|
125
|
-
yielder.yield(fetch_record_at(offset), offset)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
```
|
data/guides/best-practices.md
DELETED
@@ -1,108 +0,0 @@
|
|
1
|
-
# Best practices
|
2
|
-
|
3
|
-
## Batch iteration
|
4
|
-
|
5
|
-
Regardless of the active record enumerator used in the task, `job-iteration` gem loads records in batches of 100 (by default).
|
6
|
-
The following two tasks produce equivalent database queries,
|
7
|
-
however `RecordsJob` task allows for more frequent interruptions by doing just one thing in the `each_iteration` method.
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
# bad
|
11
|
-
class BatchesJob < ApplicationJob
|
12
|
-
include JobIteration::Iteration
|
13
|
-
|
14
|
-
def build_enumerator(product_id, cursor:)
|
15
|
-
enumerator_builder.active_record_on_batches(
|
16
|
-
Comment.where(product_id: product_id),
|
17
|
-
cursor: cursor,
|
18
|
-
batch_size: 5,
|
19
|
-
)
|
20
|
-
end
|
21
|
-
|
22
|
-
def each_iteration(batch_of_comments, product_id)
|
23
|
-
batch_of_comments.each(&:destroy)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# good
|
28
|
-
class RecordsJob < ApplicationJob
|
29
|
-
include JobIteration::Iteration
|
30
|
-
|
31
|
-
def build_enumerator(product_id, cursor:)
|
32
|
-
enumerator_builder.active_record_on_records(
|
33
|
-
Comment.where(product_id: product_id),
|
34
|
-
cursor: cursor,
|
35
|
-
batch_size: 5,
|
36
|
-
)
|
37
|
-
end
|
38
|
-
|
39
|
-
def each_iteration(comment, product_id)
|
40
|
-
comment.destroy
|
41
|
-
end
|
42
|
-
end
|
43
|
-
```
|
44
|
-
|
45
|
-
## Instrumentation
|
46
|
-
|
47
|
-
Iteration leverages [`ActiveSupport::Notifications`](https://guides.rubyonrails.org/active_support_instrumentation.html)
|
48
|
-
to notify you what it's doing. You can subscribe to the following events (listed in order of job lifecycle):
|
49
|
-
|
50
|
-
- `build_enumerator.iteration`
|
51
|
-
- `throttled.iteration` (when using ThrottleEnumerator)
|
52
|
-
- `nil_enumerator.iteration`
|
53
|
-
- `resumed.iteration`
|
54
|
-
- `each_iteration.iteration`
|
55
|
-
- `not_found.iteration`
|
56
|
-
- `interrupted.iteration`
|
57
|
-
- `completed.iteration`
|
58
|
-
|
59
|
-
All events have tags including the job class name and cursor position, some add the amount of times interrupted and/or
|
60
|
-
total time the job spent running across interruptions.
|
61
|
-
|
62
|
-
```ruby
|
63
|
-
# config/initializers/instrumentation.rb
|
64
|
-
ActiveSupport::Notifications.monotonic_subscribe("each_iteration.iteration") do |_, started, finished, _, tags|
|
65
|
-
elapsed = finished - started
|
66
|
-
StatsD.distribution(
|
67
|
-
"iteration.each_iteration",
|
68
|
-
elapsed,
|
69
|
-
tags: { job_class: tags[:job_class]&.underscore }
|
70
|
-
)
|
71
|
-
|
72
|
-
if elapsed >= BackgroundQueue.max_iteration_runtime
|
73
|
-
Rails.logger.warn "[Iteration] job_class=#{tags[:job_class]} " \
|
74
|
-
"each_iteration runtime exceeded limit of #{BackgroundQueue.max_iteration_runtime}s"
|
75
|
-
end
|
76
|
-
end
|
77
|
-
```
|
78
|
-
|
79
|
-
## Max iteration time
|
80
|
-
|
81
|
-
As you may notice in the snippet above, at Shopify we enforce that `each_iteration` does not take longer than `BackgroundQueue.max_iteration_runtime`, which is set to `25` seconds.
|
82
|
-
|
83
|
-
We discourage that because jobs with a long `each_iteration` make interruptibility somewhat useless, as the infrastructure will have to wait longer for the job to interrupt.
|
84
|
-
|
85
|
-
## Max job runtime
|
86
|
-
|
87
|
-
If a job is supposed to have millions of iterations and you expect it to run for hours and days, it's still a good idea to sometimes interrupt the job even if there are no interruption signals coming from deploys or the infrastructure. At Shopify, we interrupt at least every 5 minutes to preserve **worker capacity**.
|
88
|
-
|
89
|
-
```ruby
|
90
|
-
JobIteration.max_job_runtime = 5.minutes # nil by default
|
91
|
-
```
|
92
|
-
|
93
|
-
Use this accessor to tweak how often you'd like the job to interrupt itself.
|
94
|
-
|
95
|
-
### Per job max job runtime
|
96
|
-
|
97
|
-
For more granular control, `job_iteration_max_job_runtime` can be set **per-job class**. This allows both incremental adoption, as well as using a conservative global setting, and an aggressive setting on a per-job basis.
|
98
|
-
|
99
|
-
```ruby
|
100
|
-
class MyJob < ApplicationJob
|
101
|
-
include JobIteration::Iteration
|
102
|
-
|
103
|
-
self.job_iteration_max_job_runtime = 3.minutes
|
104
|
-
|
105
|
-
# ...
|
106
|
-
```
|
107
|
-
|
108
|
-
This setting will be inherited by any child classes, although it can be further overridden. Note that no class can **increase** the `max_job_runtime` it has inherited; it can only be **decreased**. No job can increase its `max_job_runtime` beyond the global limit.
|