job-iteration 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +35 -20
- data/.gitignore +1 -1
- data/.rubocop.yml +6 -10
- data/.ruby-version +1 -0
- data/CHANGELOG.md +26 -1
- data/Gemfile +12 -0
- data/Gemfile.lock +29 -35
- data/README.md +1 -1
- data/dev.yml +7 -8
- data/gemfiles/rails_gems.gemfile +18 -0
- data/guides/custom-enumerator.md +20 -8
- data/guides/iteration-how-it-works.md +8 -0
- data/isogun.yml +0 -1
- data/job-iteration.gemspec +0 -1
- data/lib/job-iteration/active_record_cursor.rb +11 -12
- data/lib/job-iteration/active_record_enumerator.rb +5 -5
- data/lib/job-iteration/csv_enumerator.rb +1 -1
- data/lib/job-iteration/enumerator_builder.rb +0 -3
- data/lib/job-iteration/interruption_adapters/null_adapter.rb +14 -0
- data/lib/job-iteration/interruption_adapters/resque_adapter.rb +38 -0
- data/lib/job-iteration/interruption_adapters/sidekiq_adapter.rb +30 -0
- data/lib/job-iteration/interruption_adapters.rb +52 -0
- data/lib/job-iteration/iteration.rb +26 -27
- data/lib/job-iteration/railtie.rb +12 -0
- data/lib/job-iteration/version.rb +1 -1
- data/lib/job-iteration.rb +33 -35
- metadata +10 -24
- data/gemfiles/rails_5_2.gemfile +0 -6
- data/gemfiles/rails_6_0.gemfile +0 -6
- data/gemfiles/rails_6_1.gemfile +0 -12
- data/gemfiles/rails_7_0.gemfile +0 -6
- data/gemfiles/rails_edge.gemfile +0 -6
- data/lib/job-iteration/integrations/resque.rb +0 -24
- data/lib/job-iteration/integrations/sidekiq.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cdace86b05a5a1d98777e2310d85d75679073ca63b5e52b0e83bd000891198cf
|
4
|
+
data.tar.gz: 4ed5b475931e24bf56d2b8b126bd9354064c438d0ea16f67c10276fb92ccd97b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf9d7a54a8881146a4a0a1750e99dddf8d32c9db3339411fc61c8477a89f5766e9bb99f493257e4484d56e18ef443cb6a2a0300958342774d2e7ebd2b3e23c32
|
7
|
+
data.tar.gz: 966c39a5e89ae26343e07000111ba582724b85b4bf9b7a17f790a10b679e297dca94105d3fb42830f987498fb8982037c80cf3e8130b8bede845f9c7532ade41
|
data/.github/workflows/ci.yml
CHANGED
@@ -5,8 +5,8 @@ on: [push, pull_request]
|
|
5
5
|
jobs:
|
6
6
|
build:
|
7
7
|
runs-on: ubuntu-latest
|
8
|
-
name: Ruby ${{ matrix.ruby }} | Gemfile ${{ matrix.gemfile }}
|
9
|
-
continue-on-error: ${{ matrix.
|
8
|
+
name: Ruby ${{ matrix.ruby }} | Rails ${{ matrix.rails }} | Gemfile ${{ matrix.gemfile }}
|
9
|
+
continue-on-error: ${{ matrix.rails == 'edge' }}
|
10
10
|
services:
|
11
11
|
redis:
|
12
12
|
image: redis
|
@@ -14,34 +14,53 @@ jobs:
|
|
14
14
|
- 6379:6379
|
15
15
|
strategy:
|
16
16
|
matrix:
|
17
|
-
ruby: ["2.6", "2.7", "3.0", "3.1", "3.2"]
|
18
|
-
|
17
|
+
ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"]
|
18
|
+
rails: ["5.2", "6.0", "6.1", "7.0", "7.1", "edge"]
|
19
|
+
gemfile: [rails_gems]
|
19
20
|
exclude:
|
20
21
|
- ruby: "2.6"
|
21
|
-
|
22
|
+
rails: "7.0"
|
22
23
|
- ruby: "2.6"
|
23
|
-
|
24
|
+
rails: "7.1"
|
25
|
+
- ruby: "2.6"
|
26
|
+
rails: "edge"
|
27
|
+
- ruby: "2.7"
|
28
|
+
rails: "7.1"
|
29
|
+
- ruby: "2.7"
|
30
|
+
rails: "edge"
|
31
|
+
- ruby: "3.0"
|
32
|
+
rails: "5.2"
|
33
|
+
- ruby: "3.0"
|
34
|
+
rails: "7.1"
|
24
35
|
- ruby: "3.0"
|
25
|
-
|
36
|
+
rails: "edge"
|
26
37
|
- ruby: "3.1"
|
27
|
-
|
28
|
-
- ruby: "3.2"
|
29
|
-
gemfile: rails_5_2
|
38
|
+
rails: "5.2"
|
30
39
|
- ruby: "3.1"
|
31
|
-
|
40
|
+
rails: "6.0"
|
41
|
+
- ruby: "3.2"
|
42
|
+
rails: "5.2"
|
32
43
|
- ruby: "3.2"
|
33
|
-
|
44
|
+
rails: "6.0"
|
34
45
|
- ruby: "3.2"
|
35
|
-
|
46
|
+
rails: "6.1"
|
47
|
+
- ruby: "3.3"
|
48
|
+
rails: "5.2"
|
49
|
+
- ruby: "3.3"
|
50
|
+
rails: "6.0"
|
51
|
+
- ruby: "3.3"
|
52
|
+
rails: "6.1"
|
36
53
|
|
37
54
|
include:
|
38
55
|
- ruby: head
|
39
|
-
|
56
|
+
rails: "edge"
|
57
|
+
gemfile: rails_gems
|
40
58
|
env:
|
41
59
|
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
60
|
+
RAILS_VERSION: ${{ matrix.rails }}
|
42
61
|
steps:
|
43
62
|
- name: Check out code
|
44
|
-
uses: actions/checkout@
|
63
|
+
uses: actions/checkout@v4
|
45
64
|
- name: Set up Ruby ${{ matrix.ruby }}
|
46
65
|
uses: ruby/setup-ruby@v1
|
47
66
|
with:
|
@@ -53,20 +72,16 @@ jobs:
|
|
53
72
|
mysql -uroot -h localhost -proot -e "CREATE DATABASE job_iteration_test;"
|
54
73
|
- name: Ruby tests
|
55
74
|
run: bundle exec rake test
|
56
|
-
env:
|
57
|
-
REDIS_HOST: localhost
|
58
|
-
REDIS_PORT: ${{ job.services.redis.ports[6379] }}
|
59
75
|
|
60
76
|
lint:
|
61
77
|
runs-on: ubuntu-latest
|
62
78
|
name: Lint
|
63
79
|
steps:
|
64
80
|
- name: Check out code
|
65
|
-
uses: actions/checkout@
|
81
|
+
uses: actions/checkout@v4
|
66
82
|
- name: Set up Ruby
|
67
83
|
uses: ruby/setup-ruby@v1
|
68
84
|
with:
|
69
|
-
ruby-version: "3.2"
|
70
85
|
bundler-cache: true
|
71
86
|
- name: Rubocop
|
72
87
|
run: bundle exec rubocop
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,20 +1,16 @@
|
|
1
1
|
inherit_gem:
|
2
2
|
rubocop-shopify: rubocop.yml
|
3
3
|
|
4
|
+
inherit_mode:
|
5
|
+
merge:
|
6
|
+
- Include
|
7
|
+
|
4
8
|
AllCops:
|
5
|
-
|
6
|
-
|
7
|
-
- "vendor/bundle/**/*"
|
9
|
+
Include:
|
10
|
+
- '**/*.gemfile'
|
8
11
|
Lint/SuppressedException:
|
9
12
|
Exclude:
|
10
13
|
- lib/job-iteration.rb
|
11
|
-
Style/GlobalVars:
|
12
|
-
Exclude:
|
13
|
-
- lib/job-iteration/integrations/resque.rb
|
14
14
|
Naming/FileName:
|
15
15
|
Exclude:
|
16
16
|
- lib/job-iteration.rb
|
17
|
-
Style/MethodCallWithArgsParentheses:
|
18
|
-
Exclude:
|
19
|
-
- "gemfiles/*"
|
20
|
-
- Gemfile
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.0
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,32 @@
|
|
1
1
|
### Main (unreleased)
|
2
|
-
|
3
2
|
Nil
|
4
3
|
|
4
|
+
## v1.5.0 (May 29, 2024)
|
5
|
+
### Changes
|
6
|
+
|
7
|
+
- [437](https://github.com/Shopify/job-iteration/pull/437) - Use minimum between per-class `job_iteration_max_job_runtime` and `JobIteration.max_job_runtime`, instead of enforcing only setting decreasing values.
|
8
|
+
Because it is possible to change the global or parent values after setting the value on a class, it is not possible to truly enforce the decreasing value constraint. Instead, we now use the minimum between the global value and per-class value. This is considered a non-breaking change, as it should not break any **existing** code, it only removes the constraint on new classes.
|
9
|
+
- [443](https://github.com/Shopify/job-iteration/pull/443) - Use Sidekiq `:quit` callback to detect graceful shutdown. This makes job-iteration compatible with Sidekiq run in embedded mode.
|
10
|
+
- [445](https://github.com/Shopify/job-iteration/pull/445) - Add the `around_iterate` callback, which runs around each call of `each_iteration`. This adds extensibility to build some generic handlers, such as metrics collection and logging.
|
11
|
+
- [450](https://github.com/Shopify/job-iteration/pull/450) - Infer which interruption adapter to use from the queue adapter of the job. This deprecates setting `JobIteration.interruption_adapter = <callable>`, in favor of `JobIteration.register_interruption_adapter(<queue adapter name>, <callable>)`. `JobIteration.interruption_adapter` will be removed in a future release.
|
12
|
+
|
13
|
+
### Bug fixes
|
14
|
+
|
15
|
+
- [437](https://github.com/Shopify/job-iteration/pull/437) - Defer reading `JobIteration.max_job_runtime` until runtime, instead of closing around the value at the time of job definition.
|
16
|
+
- [431](https://github.com/Shopify/job-iteration/pull/431) - Use `#id_value` instead of `send(:id)`
|
17
|
+
when generating position for cursor based on `:id` column (Rails 7.1 and above, where composite
|
18
|
+
primary models are now supported). This ensures we grab the value of the id column, rather than a
|
19
|
+
potentially composite primary key value.
|
20
|
+
- [456](https://github.com/Shopify/job-iteration/pull/431) - Use Arel to generate SQL that's type compatible for the
|
21
|
+
cursor pagination conditionals in ActiveRecord cursor. Previously, the cursor would coerce numeric ids to a string value
|
22
|
+
(e.g.: `... AND id > '1'`)
|
23
|
+
|
24
|
+
## v1.4.1 (Sep 5, 2023)
|
25
|
+
|
26
|
+
### Bug fixes
|
27
|
+
|
28
|
+
- [427](https://github.com/Shopify/job-iteration/pull/427) - Use the Rails application logger. Changes from [338](https://github.com/Shopify/job-iteration/pull/338) resulted in logging to the original value of ActiveJob.logger, not the one configured by the Rails application.
|
29
|
+
|
5
30
|
## v1.4.0 (Aug 23, 2023)
|
6
31
|
|
7
32
|
### Changes
|
data/Gemfile
CHANGED
@@ -11,6 +11,17 @@ gemspec
|
|
11
11
|
gem "sidekiq"
|
12
12
|
gem "resque"
|
13
13
|
|
14
|
+
if defined?(@rails_gems_requirements) && @rails_gems_requirements
|
15
|
+
# We avoid the `gem "..."` syntax here so Dependabot doesn't try to update these gems.
|
16
|
+
[
|
17
|
+
"activejob",
|
18
|
+
"activerecord",
|
19
|
+
].each { |name| gem name, @rails_gems_requirements }
|
20
|
+
else
|
21
|
+
# gem "activejob" # Set in gemspec
|
22
|
+
gem "activerecord"
|
23
|
+
end
|
24
|
+
|
14
25
|
gem "mysql2", github: "brianmario/mysql2"
|
15
26
|
gem "globalid"
|
16
27
|
gem "i18n"
|
@@ -22,6 +33,7 @@ gem "mocha"
|
|
22
33
|
gem "rubocop-shopify", require: false
|
23
34
|
gem "yard"
|
24
35
|
gem "rake"
|
36
|
+
gem "csv" # required for Ruby 3.4+
|
25
37
|
|
26
38
|
# for unit testing optional sorbet support
|
27
39
|
gem "sorbet-runtime"
|
data/Gemfile.lock
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
GIT
|
2
2
|
remote: https://github.com/brianmario/mysql2
|
3
|
-
revision:
|
3
|
+
revision: 43ea8af635f5e23f054294ef7759320d47f30e5f
|
4
4
|
specs:
|
5
|
-
mysql2 (0.5.
|
5
|
+
mysql2 (0.5.6)
|
6
6
|
|
7
7
|
PATH
|
8
8
|
remote: .
|
9
9
|
specs:
|
10
|
-
job-iteration (1.
|
10
|
+
job-iteration (1.5.0)
|
11
11
|
activejob (>= 5.2)
|
12
12
|
|
13
13
|
GEM
|
@@ -28,61 +28,58 @@ GEM
|
|
28
28
|
tzinfo (~> 2.0)
|
29
29
|
ast (2.4.2)
|
30
30
|
coderay (1.1.3)
|
31
|
-
concurrent-ruby (1.2.
|
31
|
+
concurrent-ruby (1.2.3)
|
32
32
|
connection_pool (2.4.1)
|
33
|
+
csv (3.3.0)
|
33
34
|
globalid (1.1.0)
|
34
35
|
activesupport (>= 5.0)
|
35
|
-
i18n (1.14.
|
36
|
+
i18n (1.14.4)
|
36
37
|
concurrent-ruby (~> 1.0)
|
37
|
-
json (2.
|
38
|
+
json (2.7.1)
|
38
39
|
language_server-protocol (3.17.0.3)
|
39
40
|
method_source (1.0.0)
|
40
41
|
minitest (5.19.0)
|
41
|
-
mocha (2.
|
42
|
+
mocha (2.2.0)
|
42
43
|
ruby2_keywords (>= 0.0.5)
|
43
44
|
mono_logger (1.1.2)
|
44
45
|
multi_json (1.15.0)
|
45
|
-
|
46
|
-
|
47
|
-
parallel (1.23.0)
|
48
|
-
parser (3.2.2.3)
|
46
|
+
parallel (1.24.0)
|
47
|
+
parser (3.3.0.5)
|
49
48
|
ast (~> 2.4.1)
|
50
49
|
racc
|
51
50
|
pry (0.14.2)
|
52
51
|
coderay (~> 1.1)
|
53
52
|
method_source (~> 1.0)
|
54
|
-
racc (1.7.
|
55
|
-
rack (
|
56
|
-
rack-protection (3.1.0)
|
57
|
-
rack (~> 2.2, >= 2.2.4)
|
53
|
+
racc (1.7.3)
|
54
|
+
rack (3.0.9.1)
|
58
55
|
rainbow (3.1.1)
|
59
|
-
rake (13.
|
60
|
-
redis (5.0
|
61
|
-
redis-client (>= 0.
|
62
|
-
redis-client (0.
|
56
|
+
rake (13.2.1)
|
57
|
+
redis (5.2.0)
|
58
|
+
redis-client (>= 0.22.0)
|
59
|
+
redis-client (0.22.1)
|
63
60
|
connection_pool
|
64
61
|
redis-namespace (1.11.0)
|
65
62
|
redis (>= 4)
|
66
|
-
regexp_parser (2.
|
63
|
+
regexp_parser (2.9.0)
|
67
64
|
resque (2.6.0)
|
68
65
|
mono_logger (~> 1.0)
|
69
66
|
multi_json (~> 1.0)
|
70
67
|
redis-namespace (~> 1.6)
|
71
68
|
sinatra (>= 0.9.2)
|
72
|
-
rexml (3.2.
|
73
|
-
rubocop (1.
|
69
|
+
rexml (3.2.6)
|
70
|
+
rubocop (1.62.1)
|
74
71
|
json (~> 2.3)
|
75
72
|
language_server-protocol (>= 3.17.0)
|
76
73
|
parallel (~> 1.10)
|
77
|
-
parser (>= 3.
|
74
|
+
parser (>= 3.3.0.2)
|
78
75
|
rainbow (>= 2.2.2, < 4.0)
|
79
76
|
regexp_parser (>= 1.8, < 3.0)
|
80
77
|
rexml (>= 3.2.5, < 4.0)
|
81
|
-
rubocop-ast (>= 1.
|
78
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
82
79
|
ruby-progressbar (~> 1.7)
|
83
80
|
unicode-display_width (>= 2.4.0, < 3.0)
|
84
|
-
rubocop-ast (1.
|
85
|
-
parser (>= 3.
|
81
|
+
rubocop-ast (1.31.2)
|
82
|
+
parser (>= 3.3.0.4)
|
86
83
|
rubocop-shopify (2.14.0)
|
87
84
|
rubocop (~> 1.51)
|
88
85
|
ruby-progressbar (1.13.0)
|
@@ -92,23 +89,20 @@ GEM
|
|
92
89
|
connection_pool (>= 2.3.0)
|
93
90
|
rack (>= 2.2.4)
|
94
91
|
redis-client (>= 0.14.0)
|
95
|
-
sinatra (
|
96
|
-
|
97
|
-
rack (~> 2.2, >= 2.2.4)
|
98
|
-
rack-protection (= 3.1.0)
|
99
|
-
tilt (~> 2.0)
|
92
|
+
sinatra (1.0)
|
93
|
+
rack (>= 1.0)
|
100
94
|
sorbet-runtime (0.5.10978)
|
101
|
-
tilt (2.2.0)
|
102
95
|
tzinfo (2.0.6)
|
103
96
|
concurrent-ruby (~> 1.0)
|
104
|
-
unicode-display_width (2.
|
105
|
-
yard (0.9.
|
97
|
+
unicode-display_width (2.5.0)
|
98
|
+
yard (0.9.36)
|
106
99
|
|
107
100
|
PLATFORMS
|
108
101
|
ruby
|
109
102
|
|
110
103
|
DEPENDENCIES
|
111
104
|
activerecord
|
105
|
+
csv
|
112
106
|
globalid
|
113
107
|
i18n
|
114
108
|
job-iteration!
|
@@ -124,4 +118,4 @@ DEPENDENCIES
|
|
124
118
|
yard
|
125
119
|
|
126
120
|
BUNDLED WITH
|
127
|
-
2.
|
121
|
+
2.5.7
|
data/README.md
CHANGED
@@ -185,7 +185,7 @@ There a few configuration assumptions that are required for Iteration to work wi
|
|
185
185
|
|
186
186
|
**What happens when my job is interrupted?** A checkpoint will be persisted to Redis after the current `each_iteration`, and the job will be re-enqueued. Once it's popped off the queue, the worker will work off from the next iteration.
|
187
187
|
|
188
|
-
**What happens with retries?** An interruption of a job does not count as a retry.
|
188
|
+
**What happens with retries?** An interruption of a job does not count as a retry. If an exception occurs, the job will retry or be discarded as normal using Active Job configuration for the job. If the job retries, it processes the iteration that originally failed and progress will continue from there on if successful.
|
189
189
|
|
190
190
|
**What happens if my iteration takes a long time?** We recommend that a single `each_iteration` should take no longer than 30 seconds. In the future, this may raise an exception.
|
191
191
|
|
data/dev.yml
CHANGED
@@ -3,17 +3,16 @@
|
|
3
3
|
name: job-iteration
|
4
4
|
|
5
5
|
up:
|
6
|
-
-
|
7
|
-
|
8
|
-
|
9
|
-
- ruby:
|
10
|
-
version: 2.7.6
|
11
|
-
- isogun
|
6
|
+
- packages:
|
7
|
+
- mysql_client
|
8
|
+
- ruby
|
12
9
|
- bundler
|
10
|
+
- mysql
|
11
|
+
- redis
|
13
12
|
- custom:
|
14
13
|
name: Create Job Iteration database
|
15
|
-
meet: mysql -uroot -h
|
16
|
-
met?: mysql -uroot -h
|
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
|
17
16
|
|
18
17
|
commands:
|
19
18
|
test:
|
@@ -0,0 +1,18 @@
|
|
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
|
data/guides/custom-enumerator.md
CHANGED
@@ -1,19 +1,25 @@
|
|
1
|
-
|
1
|
+
# Custom Enumerator
|
2
|
+
|
3
|
+
`Iteration` leverages the [Enumerator](https://ruby-doc.org/3.2.1/Enumerator.html) pattern from the Ruby standard library,
|
2
4
|
which allows us to use almost any resource as a collection to iterate.
|
3
5
|
|
4
6
|
Before writing an enumerator, it is important to understand [how Iteration works](iteration-how-it-works.md) and how
|
5
7
|
your enumerator will be used by it. An enumerator must `yield` two things in the following order as positional
|
6
8
|
arguments:
|
7
9
|
- An object to be processed in a job `each_iteration` method
|
8
|
-
- A cursor position, which Iteration will persist if `each_iteration` returns
|
10
|
+
- A cursor position, which `Iteration` will persist if `each_iteration` returns successfully and the job is forced to shut
|
9
11
|
down. It can be any data type your job backend can serialize and deserialize correctly.
|
10
12
|
|
11
|
-
A job that includes Iteration is first started with `nil` as the cursor. When resuming an interrupted job, Iteration
|
13
|
+
A job that includes `Iteration` is first started with `nil` as the cursor. When resuming an interrupted job, `Iteration`
|
12
14
|
will deserialize the persisted cursor and pass it to the job's `build_enumerator` method, which your enumerator uses to
|
13
15
|
find objects that come _after_ the last successfully processed object. The [array enumerator](https://github.com/Shopify/job-iteration/blob/v1.3.6/lib/job-iteration/enumerator_builder.rb#L50-L67)
|
14
16
|
is a simple example which uses the array index as the cursor position.
|
15
17
|
|
16
|
-
|
18
|
+
In addition to the remainder of this guide, we recommend you read the implementation of the other enumerators that come with the library (`CsvEnumerator`, `ActiveRecordEnumerator`) to gain a better understanding of building enumerators.
|
19
|
+
|
20
|
+
## Enumerator with cursor
|
21
|
+
|
22
|
+
For a more complex example, consider this `Enumerator` that wraps a third party API (Stripe) for paginated iteration and
|
17
23
|
stores a string as the cursor position:
|
18
24
|
|
19
25
|
```ruby
|
@@ -58,6 +64,8 @@ class StripeListEnumerator
|
|
58
64
|
end
|
59
65
|
```
|
60
66
|
|
67
|
+
### Usage
|
68
|
+
|
61
69
|
Here we leverage the Stripe cursor pagination where the cursor is an ID of a specific item in the collection. The job
|
62
70
|
which uses such an `Enumerator` would then look like so:
|
63
71
|
|
@@ -90,12 +98,14 @@ end
|
|
90
98
|
and you initiate the job with
|
91
99
|
|
92
100
|
```ruby
|
93
|
-
LoadRefundsForChargeJob.perform_later(
|
101
|
+
LoadRefundsForChargeJob.perform_later(charge_id = "chrg_345")
|
94
102
|
```
|
95
103
|
|
96
|
-
|
104
|
+
## Cursorless enumerator
|
105
|
+
|
106
|
+
Sometimes you can ignore the cursor. Consider the following custom `Enumerator` that takes items from a Redis list, which
|
97
107
|
is essentially a queue. Even if this job doesn't need to persist a cursor in order to resume, it can still use
|
98
|
-
Iteration's signal handling to finish `each_iteration` and gracefully terminate.
|
108
|
+
`Iteration`'s signal handling to finish `each_iteration` and gracefully terminate.
|
99
109
|
|
100
110
|
```ruby
|
101
111
|
class RedisPopListJob < ActiveJob::Base
|
@@ -115,7 +125,9 @@ class RedisPopListJob < ActiveJob::Base
|
|
115
125
|
end
|
116
126
|
```
|
117
127
|
|
118
|
-
|
128
|
+
## Caveats
|
129
|
+
|
130
|
+
### Post-`yield` code
|
119
131
|
|
120
132
|
Code that is written after the `yield` in a custom enumerator is not guaranteed to execute. In the case that a job is
|
121
133
|
forced to exit ie `job_should_exit?` is true, then the job is re-enqueued during the yield and the rest of the code in
|
@@ -21,6 +21,14 @@ SELECT `products`.* FROM `products` ORDER BY products.id LIMIT 100
|
|
21
21
|
SELECT `products`.* FROM `products` WHERE (products.id > 2) ORDER BY products.id LIMIT 100
|
22
22
|
```
|
23
23
|
|
24
|
+
## Exceptions inside `each_iteration`
|
25
|
+
|
26
|
+
Unrescued exceptions inside the `each_iteration` block are handled the same way as exceptions occuring in `perform` for a regular Active Job subclass, meaning you need to configure it to retry using [`retry_on`](https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on) or manually call [`retry_job`](https://api.rubyonrails.org/classes/ActiveJob/Exceptions.html#method-i-retry_job). The job will re-enqueue itself with the last successful cursor, the iteration that failed will be retried with the same parameters and the cursor will only move if that iteration succeeds. This behaviour may be enough for intermittent errors, such as network connection failures, but if your execution is deterministic and you have an error, subsequent iterations will never run.
|
27
|
+
|
28
|
+
In other words, if you are trying to process 100 records but the job consistently fails on the 61st, only the first 60 will be processed and the job will try to process the 61st record until retries are exhausted.
|
29
|
+
|
30
|
+
If no retries are configured or retries are exhausted, Active Job 'bubbles up' the exception to the job backend. Retries by the backend (e.g. Sidekiq) are not supported, meaning that jobs retried by the job backend instead of Active Job will restart from the beginning.
|
31
|
+
|
24
32
|
## Signals
|
25
33
|
|
26
34
|
It's critical to know [UNIX signals](https://www.tutorialspoint.com/unix/unix-signals-traps.htm) in order to understand how interruption works. There are two main signals that Sidekiq and Resque use: `SIGTERM` and `SIGKILL`. `SIGTERM` is the graceful termination signal which means that the process should exit _soon_, not immediately. For Iteration, it means that we have time to wait for the last iteration to finish and to push job back to the queue with the last cursor position.
|
data/isogun.yml
CHANGED
data/job-iteration.gemspec
CHANGED
@@ -26,6 +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_development_dependency("activerecord")
|
30
29
|
spec.add_dependency("activejob", ">= 5.2")
|
31
30
|
end
|
@@ -18,12 +18,8 @@ module JobIteration
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
def initialize(relation, columns
|
22
|
-
@columns =
|
23
|
-
Array(columns)
|
24
|
-
else
|
25
|
-
Array(relation.primary_key).map { |pk| "#{relation.table_name}.#{pk}" }
|
26
|
-
end
|
21
|
+
def initialize(relation, columns, position = nil)
|
22
|
+
@columns = columns
|
27
23
|
self.position = Array.wrap(position)
|
28
24
|
raise ArgumentError, "Must specify at least one column" if columns.empty?
|
29
25
|
if relation.joins_values.present? && !@columns.all? { |column| column.to_s.include?(".") }
|
@@ -34,7 +30,7 @@ module JobIteration
|
|
34
30
|
raise ConditionNotSupportedError
|
35
31
|
end
|
36
32
|
|
37
|
-
@base_relation = relation.reorder(
|
33
|
+
@base_relation = relation.reorder(*@columns)
|
38
34
|
@reached_end = false
|
39
35
|
end
|
40
36
|
|
@@ -54,8 +50,11 @@ module JobIteration
|
|
54
50
|
|
55
51
|
def update_from_record(record)
|
56
52
|
self.position = @columns.map do |column|
|
57
|
-
|
58
|
-
|
53
|
+
if ActiveRecord.version >= Gem::Version.new("7.1.0.alpha") && column.name == "id"
|
54
|
+
record.id_value
|
55
|
+
else
|
56
|
+
record.send(column.name)
|
57
|
+
end
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
@@ -84,14 +83,14 @@ module JobIteration
|
|
84
83
|
i = @position.size - 1
|
85
84
|
column = @columns[i]
|
86
85
|
conditions = if @columns.size == @position.size
|
87
|
-
|
86
|
+
column.gt(@position[i])
|
88
87
|
else
|
89
|
-
|
88
|
+
column.gteq(@position[i])
|
90
89
|
end
|
91
90
|
while i > 0
|
92
91
|
i -= 1
|
93
92
|
column = @columns[i]
|
94
|
-
conditions =
|
93
|
+
conditions = column.gt(@position[i]).or(column.eq(@position[i]).and(conditions))
|
95
94
|
end
|
96
95
|
ret = @position.reduce([conditions]) { |params, value| params << value << value }
|
97
96
|
ret.pop
|
@@ -11,9 +11,9 @@ module JobIteration
|
|
11
11
|
@relation = relation
|
12
12
|
@batch_size = batch_size
|
13
13
|
@columns = if columns
|
14
|
-
Array(columns)
|
14
|
+
Array(columns).map { |col| relation.arel_table[col.to_sym] }
|
15
15
|
else
|
16
|
-
Array(relation.primary_key).map { |pk|
|
16
|
+
Array(relation.primary_key).map { |pk| relation.arel_table[pk.to_sym] }
|
17
17
|
end
|
18
18
|
@cursor = cursor
|
19
19
|
end
|
@@ -45,7 +45,7 @@ module JobIteration
|
|
45
45
|
|
46
46
|
def cursor_value(record)
|
47
47
|
positions = @columns.map do |column|
|
48
|
-
attribute_name = column.
|
48
|
+
attribute_name = column.name.to_sym
|
49
49
|
column_value(record, attribute_name)
|
50
50
|
end
|
51
51
|
return positions.first if positions.size == 1
|
@@ -58,8 +58,8 @@ module JobIteration
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def column_value(record, attribute)
|
61
|
-
value = record.read_attribute(attribute
|
62
|
-
case record.class.columns_hash.fetch(attribute).type
|
61
|
+
value = record.read_attribute(attribute)
|
62
|
+
case record.class.columns_hash.fetch(attribute.to_s).type
|
63
63
|
when :datetime
|
64
64
|
value.strftime(SQL_DATETIME_WITH_NSEC)
|
65
65
|
else
|
@@ -20,7 +20,7 @@ module JobIteration
|
|
20
20
|
# csv = CSV.open('tmp/files', { converters: :integer, headers: true })
|
21
21
|
# JobIteration::CsvEnumerator.new(csv).rows(cursor: cursor)
|
22
22
|
def initialize(csv)
|
23
|
-
unless csv.instance_of?(CSV)
|
23
|
+
unless defined?(CSV) && csv.instance_of?(CSV)
|
24
24
|
raise ArgumentError, "CsvEnumerator.new takes CSV object"
|
25
25
|
end
|
26
26
|
|
@@ -55,9 +55,6 @@ module JobIteration
|
|
55
55
|
unless enumerable.is_a?(Array)
|
56
56
|
raise ArgumentError, "enumerable must be an Array"
|
57
57
|
end
|
58
|
-
if enumerable.any? { |i| defined?(ActiveRecord) && i.is_a?(ActiveRecord::Base) }
|
59
|
-
raise ArgumentError, "array cannot contain ActiveRecord objects"
|
60
|
-
end
|
61
58
|
|
62
59
|
drop =
|
63
60
|
if cursor.nil?
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "resque"
|
5
|
+
rescue LoadError
|
6
|
+
# Resque is not available, no need to load the adapter
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
module JobIteration
|
11
|
+
module InterruptionAdapters
|
12
|
+
module ResqueAdapter
|
13
|
+
# @private
|
14
|
+
module IterationExtension
|
15
|
+
def initialize(*)
|
16
|
+
$resque_worker = self # rubocop:disable Style/GlobalVars
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @private
|
22
|
+
module ::Resque
|
23
|
+
class Worker
|
24
|
+
# The patch is required in order to call shutdown? on a Resque::Worker instance
|
25
|
+
prepend(IterationExtension)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def call
|
31
|
+
$resque_worker.try!(:shutdown?) # rubocop:disable Style/GlobalVars
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
register(:resque, ResqueAdapter)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "sidekiq"
|
5
|
+
rescue LoadError
|
6
|
+
# Sidekiq is not available, no need to load the adapter
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
module JobIteration
|
11
|
+
module InterruptionAdapters
|
12
|
+
module SidekiqAdapter
|
13
|
+
class << self
|
14
|
+
attr_accessor :stopping
|
15
|
+
|
16
|
+
def call
|
17
|
+
stopping
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Sidekiq.configure_server do |config|
|
22
|
+
config.on(:quiet) do
|
23
|
+
SidekiqAdapter.stopping = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register(:sidekiq, SidekiqAdapter)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "interruption_adapters/null_adapter"
|
4
|
+
|
5
|
+
module JobIteration
|
6
|
+
module InterruptionAdapters
|
7
|
+
BUNDLED_ADAPTERS = [:resque, :sidekiq].freeze # @api private
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Returns adapter for specified name.
|
11
|
+
#
|
12
|
+
# JobIteration::InterruptionAdapters.lookup(:sidekiq)
|
13
|
+
# # => JobIteration::InterruptionAdapters::SidekiqAdapter
|
14
|
+
def lookup(name)
|
15
|
+
registry.fetch(name.to_sym) do
|
16
|
+
Deprecation.warn(<<~DEPRECATION_MESSAGE, caller_locations(1))
|
17
|
+
No interruption adapter is registered for #{name.inspect}; falling back to `NullAdapter`, which never interrupts.
|
18
|
+
Use `JobIteration::InterruptionAdapters.register(#{name.to_sym.inspect}, <adapter>) to register one.
|
19
|
+
This will raise starting in version #{Deprecation.deprecation_horizon} of #{Deprecation.gem_name}!"
|
20
|
+
DEPRECATION_MESSAGE
|
21
|
+
|
22
|
+
NullAdapter
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Registers adapter for specified name.
|
27
|
+
#
|
28
|
+
# JobIteration::InterruptionAdapters.register(:sidekiq, MyCustomSidekiqAdapter)
|
29
|
+
def register(name, adapter)
|
30
|
+
raise ArgumentError, "adapter must be callable" unless adapter.respond_to?(:call)
|
31
|
+
|
32
|
+
registry[name.to_sym] = adapter
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :registry
|
38
|
+
end
|
39
|
+
|
40
|
+
@registry = {}
|
41
|
+
|
42
|
+
# Built-in Rails adapters. It doesn't make sense to interrupt for these.
|
43
|
+
register(:async, NullAdapter)
|
44
|
+
register(:inline, NullAdapter)
|
45
|
+
register(:test, NullAdapter)
|
46
|
+
|
47
|
+
# External adapters
|
48
|
+
BUNDLED_ADAPTERS.each do |name|
|
49
|
+
require_relative "interruption_adapters/#{name}_adapter"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -42,35 +42,15 @@ module JobIteration
|
|
42
42
|
|
43
43
|
included do |_base|
|
44
44
|
define_callbacks :start
|
45
|
+
define_callbacks :iterate
|
45
46
|
define_callbacks :shutdown
|
46
47
|
define_callbacks :complete
|
47
48
|
|
48
49
|
class_attribute(
|
49
50
|
:job_iteration_max_job_runtime,
|
50
|
-
|
51
|
+
instance_accessor: false,
|
51
52
|
instance_predicate: false,
|
52
|
-
default: JobIteration.max_job_runtime,
|
53
53
|
)
|
54
|
-
|
55
|
-
singleton_class.prepend(PrependedClassMethods)
|
56
|
-
end
|
57
|
-
|
58
|
-
module PrependedClassMethods
|
59
|
-
def job_iteration_max_job_runtime=(new)
|
60
|
-
existing = job_iteration_max_job_runtime
|
61
|
-
|
62
|
-
if existing && (!new || new > existing)
|
63
|
-
existing_label = existing.inspect
|
64
|
-
new_label = new ? new.inspect : "#{new.inspect} (no limit)"
|
65
|
-
raise(
|
66
|
-
ArgumentError,
|
67
|
-
"job_iteration_max_job_runtime may only decrease; " \
|
68
|
-
"#{self} tried to increase it from #{existing_label} to #{new_label}",
|
69
|
-
)
|
70
|
-
end
|
71
|
-
|
72
|
-
super
|
73
|
-
end
|
74
54
|
end
|
75
55
|
|
76
56
|
module ClassMethods
|
@@ -91,6 +71,10 @@ module JobIteration
|
|
91
71
|
set_callback(:complete, :after, *filters, &blk)
|
92
72
|
end
|
93
73
|
|
74
|
+
def around_iterate(&blk)
|
75
|
+
set_callback(:iterate, :around, &blk)
|
76
|
+
end
|
77
|
+
|
94
78
|
private
|
95
79
|
|
96
80
|
def ban_perform_definition
|
@@ -136,6 +120,10 @@ module JobIteration
|
|
136
120
|
|
137
121
|
private
|
138
122
|
|
123
|
+
def interruption_adapter # @private
|
124
|
+
JobIteration.interruption_adapter || JobIteration::InterruptionAdapters.lookup(self.class.queue_adapter_name)
|
125
|
+
end
|
126
|
+
|
139
127
|
def enumerator_builder
|
140
128
|
JobIteration.enumerator_builder.new(self)
|
141
129
|
end
|
@@ -194,7 +182,9 @@ module JobIteration
|
|
194
182
|
tags = instrumentation_tags.merge(cursor_position: cursor_from_enumerator)
|
195
183
|
ActiveSupport::Notifications.instrument("each_iteration.iteration", tags) do
|
196
184
|
found_record = true
|
197
|
-
|
185
|
+
run_callbacks(:iterate) do
|
186
|
+
each_iteration(object_from_enumerator, *arguments)
|
187
|
+
end
|
198
188
|
self.cursor_position = cursor_from_enumerator
|
199
189
|
end
|
200
190
|
|
@@ -291,11 +281,20 @@ module JobIteration
|
|
291
281
|
end
|
292
282
|
|
293
283
|
def job_should_exit?
|
294
|
-
|
295
|
-
|
296
|
-
|
284
|
+
max_job_runtime = job_iteration_max_job_runtime
|
285
|
+
return true if max_job_runtime && start_time && (Time.now.utc - start_time) > max_job_runtime
|
286
|
+
|
287
|
+
interruption_adapter.call || (defined?(super) && super)
|
288
|
+
end
|
289
|
+
|
290
|
+
def job_iteration_max_job_runtime
|
291
|
+
global_max = JobIteration.max_job_runtime
|
292
|
+
class_max = self.class.job_iteration_max_job_runtime
|
293
|
+
|
294
|
+
return global_max unless class_max
|
295
|
+
return class_max unless global_max
|
297
296
|
|
298
|
-
|
297
|
+
[global_max, class_max].min
|
299
298
|
end
|
300
299
|
|
301
300
|
def handle_completed(completed)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
return unless defined?(Rails::Railtie)
|
4
|
+
|
5
|
+
module JobIteration
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
initializer "job_iteration.register_deprecator" do |app|
|
8
|
+
# app.deprecators was added in Rails 7.1
|
9
|
+
app.deprecators[:job_iteration] = JobIteration::Deprecation if app.respond_to?(:deprecators)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/job-iteration.rb
CHANGED
@@ -3,19 +3,23 @@
|
|
3
3
|
require "active_job"
|
4
4
|
require_relative "./job-iteration/version"
|
5
5
|
require_relative "./job-iteration/enumerator_builder"
|
6
|
+
require_relative "./job-iteration/interruption_adapters"
|
6
7
|
require_relative "./job-iteration/iteration"
|
7
8
|
require_relative "./job-iteration/log_subscriber"
|
9
|
+
require_relative "./job-iteration/railtie"
|
8
10
|
|
9
11
|
module JobIteration
|
10
|
-
|
11
|
-
|
12
|
-
INTEGRATIONS = [:resque, :sidekiq]
|
12
|
+
Deprecation = ActiveSupport::Deprecation.new("2.0", "JobIteration")
|
13
13
|
|
14
14
|
extend self
|
15
15
|
|
16
|
-
|
16
|
+
attr_writer :logger
|
17
17
|
|
18
|
-
|
18
|
+
class << self
|
19
|
+
def logger
|
20
|
+
@logger || ActiveJob::Base.logger
|
21
|
+
end
|
22
|
+
end
|
19
23
|
|
20
24
|
# Use this to _always_ interrupt the job after it's been running for more than N seconds.
|
21
25
|
# @example
|
@@ -25,14 +29,22 @@ module JobIteration
|
|
25
29
|
# This setting will make it to always interrupt a job after it's been iterating for 5 minutes.
|
26
30
|
# Defaults to nil which means that jobs will not be interrupted except on termination signal.
|
27
31
|
#
|
28
|
-
# This setting can be
|
29
|
-
#
|
32
|
+
# This setting can be overriden by using the inheritable per-class job_iteration_max_job_runtime setting. At runtime,
|
33
|
+
# the lower of the two will be used.
|
30
34
|
# @example
|
31
35
|
#
|
32
36
|
# class MyJob < ActiveJob::Base
|
33
37
|
# include JobIteration::Iteration
|
34
38
|
# self.job_iteration_max_job_runtime = 1.minute
|
35
39
|
# # ...
|
40
|
+
#
|
41
|
+
# Note that if a sub-class overrides its parent's setting, only the global and sub-class setting will be considered,
|
42
|
+
# not the parent's.
|
43
|
+
# @example
|
44
|
+
#
|
45
|
+
# class ChildJob < MyJob
|
46
|
+
# self.job_iteration_max_job_runtime = 3.minutes # MyJob's 1.minute will be discarded.
|
47
|
+
# # ...
|
36
48
|
attr_accessor :max_job_runtime
|
37
49
|
|
38
50
|
# Configures a delay duration to wait before resuming an interrupted job.
|
@@ -45,10 +57,21 @@ module JobIteration
|
|
45
57
|
# where the throttle backoff value will take precedence over this setting.
|
46
58
|
attr_accessor :default_retry_backoff
|
47
59
|
|
48
|
-
|
49
|
-
|
60
|
+
attr_reader :interruption_adapter
|
61
|
+
|
62
|
+
# Overrides interruption checks based on queue adapter.
|
63
|
+
# @deprecated - Use JobIteration::InterruptionAdapters.register(:foo, callable) instead.
|
64
|
+
def interruption_adapter=(adapter)
|
65
|
+
Deprecation.warn("Setting JobIteration.interruption_adapter is deprecated. "\
|
66
|
+
"Use JobIteration::InterruptionAdapters.register(:foo, callable) instead "\
|
67
|
+
"to register the callable (a proc, method, or other object responding to #call) "\
|
68
|
+
"as the interruption adapter for queue adapter :foo.")
|
69
|
+
@interruption_adapter = adapter
|
70
|
+
end
|
50
71
|
|
51
|
-
|
72
|
+
def register_interruption_adapter(adapter_name, adapter)
|
73
|
+
InterruptionAdapters.register(adapter_name, adapter)
|
74
|
+
end
|
52
75
|
|
53
76
|
# Set if you want to use your own enumerator builder instead of default EnumeratorBuilder.
|
54
77
|
# @example
|
@@ -61,29 +84,4 @@ module JobIteration
|
|
61
84
|
attr_accessor :enumerator_builder
|
62
85
|
|
63
86
|
self.enumerator_builder = JobIteration::EnumeratorBuilder
|
64
|
-
|
65
|
-
def load_integrations
|
66
|
-
loaded = nil
|
67
|
-
INTEGRATIONS.each do |integration|
|
68
|
-
load_integration(integration)
|
69
|
-
if loaded
|
70
|
-
raise IntegrationLoadError,
|
71
|
-
"#{loaded} integration has already been loaded, but #{integration} is also available. " \
|
72
|
-
"Iteration will only work with one integration."
|
73
|
-
end
|
74
|
-
loaded = integration
|
75
|
-
rescue LoadError
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def load_integration(integration)
|
80
|
-
unless INTEGRATIONS.include?(integration)
|
81
|
-
raise IntegrationLoadError,
|
82
|
-
"#{integration} integration is not supported. Available integrations: #{INTEGRATIONS.join(", ")}"
|
83
|
-
end
|
84
|
-
|
85
|
-
require_relative "./job-iteration/integrations/#{integration}"
|
86
|
-
end
|
87
87
|
end
|
88
|
-
|
89
|
-
JobIteration.load_integrations unless ENV["ITERATION_DISABLE_AUTOCONFIGURE"]
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: job-iteration
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: activerecord
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: activejob
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -50,6 +36,7 @@ files:
|
|
50
36
|
- ".github/workflows/cla.yml"
|
51
37
|
- ".gitignore"
|
52
38
|
- ".rubocop.yml"
|
39
|
+
- ".ruby-version"
|
53
40
|
- ".yardopts"
|
54
41
|
- CHANGELOG.md
|
55
42
|
- CODE_OF_CONDUCT.md
|
@@ -61,11 +48,7 @@ files:
|
|
61
48
|
- bin/setup
|
62
49
|
- bin/test
|
63
50
|
- dev.yml
|
64
|
-
- gemfiles/
|
65
|
-
- gemfiles/rails_6_0.gemfile
|
66
|
-
- gemfiles/rails_6_1.gemfile
|
67
|
-
- gemfiles/rails_7_0.gemfile
|
68
|
-
- gemfiles/rails_edge.gemfile
|
51
|
+
- gemfiles/rails_gems.gemfile
|
69
52
|
- guides/argument-semantics.md
|
70
53
|
- guides/best-practices.md
|
71
54
|
- guides/custom-enumerator.md
|
@@ -79,11 +62,14 @@ files:
|
|
79
62
|
- lib/job-iteration/active_record_enumerator.rb
|
80
63
|
- lib/job-iteration/csv_enumerator.rb
|
81
64
|
- lib/job-iteration/enumerator_builder.rb
|
82
|
-
- lib/job-iteration/
|
83
|
-
- lib/job-iteration/
|
65
|
+
- lib/job-iteration/interruption_adapters.rb
|
66
|
+
- lib/job-iteration/interruption_adapters/null_adapter.rb
|
67
|
+
- lib/job-iteration/interruption_adapters/resque_adapter.rb
|
68
|
+
- lib/job-iteration/interruption_adapters/sidekiq_adapter.rb
|
84
69
|
- lib/job-iteration/iteration.rb
|
85
70
|
- lib/job-iteration/log_subscriber.rb
|
86
71
|
- lib/job-iteration/nested_enumerator.rb
|
72
|
+
- lib/job-iteration/railtie.rb
|
87
73
|
- lib/job-iteration/test_helper.rb
|
88
74
|
- lib/job-iteration/throttle_enumerator.rb
|
89
75
|
- lib/job-iteration/version.rb
|
@@ -108,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
94
|
- !ruby/object:Gem::Version
|
109
95
|
version: '0'
|
110
96
|
requirements: []
|
111
|
-
rubygems_version: 3.
|
97
|
+
rubygems_version: 3.5.10
|
112
98
|
signing_key:
|
113
99
|
specification_version: 4
|
114
100
|
summary: Makes your background jobs interruptible and resumable.
|
data/gemfiles/rails_5_2.gemfile
DELETED
data/gemfiles/rails_6_0.gemfile
DELETED
data/gemfiles/rails_6_1.gemfile
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
eval_gemfile "../Gemfile"
|
4
|
-
|
5
|
-
if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new("3.1")
|
6
|
-
gem "net-imap", require: false
|
7
|
-
gem "net-pop", require: false
|
8
|
-
gem "net-smtp", require: false
|
9
|
-
end
|
10
|
-
|
11
|
-
gem "activejob", "~> 6.1.0"
|
12
|
-
gem "activerecord", "~> 6.1.0"
|
data/gemfiles/rails_7_0.gemfile
DELETED
data/gemfiles/rails_edge.gemfile
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "resque"
|
4
|
-
|
5
|
-
module JobIteration
|
6
|
-
module Integrations
|
7
|
-
module ResqueIterationExtension # @private
|
8
|
-
def initialize(*) # @private
|
9
|
-
$resque_worker = self
|
10
|
-
super
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
# @private
|
15
|
-
module ::Resque
|
16
|
-
class Worker
|
17
|
-
# The patch is required in order to call shutdown? on a Resque::Worker instance
|
18
|
-
prepend(ResqueIterationExtension)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
JobIteration.interruption_adapter = -> { $resque_worker.try!(:shutdown?) }
|
23
|
-
end
|
24
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "sidekiq"
|
4
|
-
|
5
|
-
module JobIteration
|
6
|
-
module Integrations # @private
|
7
|
-
JobIteration.interruption_adapter = -> do
|
8
|
-
if defined?(Sidekiq::CLI) && Sidekiq::CLI.instance
|
9
|
-
Sidekiq::CLI.instance.launcher.stopping?
|
10
|
-
else
|
11
|
-
false
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|