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.
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
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb"]
10
- end
11
-
12
- task(default: :test)
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
@@ -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
- ```
@@ -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.