job-iteration 1.3.6 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f1dea81ba8267470285bbd858ace091200060cc0695dfa8a986293139806a4d
4
- data.tar.gz: 23affca4d929d7bfafbf9989eb152a65fe3831d380b79b877a31469854768871
3
+ metadata.gz: 6f875dad03c85d6eb2d3f25d2dd799e6b26082c90e3f70237e7c7a813b513e9d
4
+ data.tar.gz: 3bd26bbaf398285c98a60c1f6f84daacd47d7ff8f21d10f91bcb37a8fce6071d
5
5
  SHA512:
6
- metadata.gz: 163fd0b5034c8e0f06fc6f3e8c39f31a8b02286a7d51cc7602ead690cf8696299883c334bf5c57414dc7ba07f48faec157fdc7c602e61cbc2a42b398e23e39aa
7
- data.tar.gz: 74634403a068cbccdce59b9ca526cbb97a83dbef2f34dad535385e3d4984d1967dd43748ee32ac0c80fa520ce2a137c33be15e32c2ed0a05e3c7957f85173445
6
+ metadata.gz: 556835d8b4d7c9d1954936e758fb2c7ecde63063b7260e21e88e5118a603db76dd8689bea28066ea5695c147b2ae7084ed5743450da8b0c62f55463be11c6878
7
+ data.tar.gz: 05042e53baa957afd99f75d6975958175187dfe77f076d9776c5cf1251b50fd1cb5fda98ddbc39491b60f4cdd6a10036c7f7598dc708cf7ab54a22cb0bfcd796
@@ -6,14 +6,15 @@ jobs:
6
6
  build:
7
7
  runs-on: ubuntu-latest
8
8
  name: Ruby ${{ matrix.ruby }} | Gemfile ${{ matrix.gemfile }}
9
+ continue-on-error: ${{ matrix.gemfile == 'rails_edge' }}
9
10
  services:
10
11
  redis:
11
12
  image: redis
12
13
  ports:
13
- - 6379:6379
14
+ - 6379:6379
14
15
  strategy:
15
16
  matrix:
16
- ruby: ["2.6", "2.7", "3.0", "3.1"]
17
+ ruby: ["2.6", "2.7", "3.0", "3.1", "3.2"]
17
18
  gemfile: [rails_5_2, rails_6_0, rails_6_1, rails_7_0, rails_edge]
18
19
  exclude:
19
20
  - ruby: "2.6"
@@ -24,31 +25,50 @@ jobs:
24
25
  gemfile: rails_5_2
25
26
  - ruby: "3.1"
26
27
  gemfile: rails_5_2
28
+ - ruby: "3.2"
29
+ gemfile: rails_5_2
27
30
  - ruby: "3.1"
28
31
  gemfile: rails_6_0
32
+ - ruby: "3.2"
33
+ gemfile: rails_6_0
34
+ - ruby: "3.2"
35
+ gemfile: rails_6_1
36
+
29
37
  include:
30
38
  - ruby: head
31
39
  gemfile: rails_edge
32
40
  env:
33
41
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
34
42
  steps:
35
- - name: Check out code
36
- uses: actions/checkout@v3
37
- - name: Set up Ruby ${{ matrix.ruby }}
38
- uses: ruby/setup-ruby@v1
39
- with:
40
- ruby-version: ${{ matrix.ruby }}
41
- bundler-cache: true
42
- - name: Start MySQL and create DB
43
- run: |
44
- sudo systemctl start mysql.service
45
- mysql -uroot -h localhost -proot -e "CREATE DATABASE job_iteration_test;"
46
- - name: Rubocop
47
- run: bundle exec rubocop
48
- - name: Ruby tests
49
- run: bundle exec rake test
50
- env:
51
- REDIS_HOST: localhost
52
- REDIS_PORT: ${{ job.services.redis.ports[6379] }}
53
- - name: Documentation correctly written
54
- run: bundle exec yardoc --no-output --no-save --no-stats --fail-on-warning
43
+ - name: Check out code
44
+ uses: actions/checkout@v3
45
+ - name: Set up Ruby ${{ matrix.ruby }}
46
+ uses: ruby/setup-ruby@v1
47
+ with:
48
+ ruby-version: ${{ matrix.ruby }}
49
+ bundler-cache: true
50
+ - name: Start MySQL and create DB
51
+ run: |
52
+ sudo systemctl start mysql.service
53
+ mysql -uroot -h localhost -proot -e "CREATE DATABASE job_iteration_test;"
54
+ - name: Ruby tests
55
+ run: bundle exec rake test
56
+ env:
57
+ REDIS_HOST: localhost
58
+ REDIS_PORT: ${{ job.services.redis.ports[6379] }}
59
+
60
+ lint:
61
+ runs-on: ubuntu-latest
62
+ name: Lint
63
+ steps:
64
+ - name: Check out code
65
+ uses: actions/checkout@v3
66
+ - name: Set up Ruby
67
+ uses: ruby/setup-ruby@v1
68
+ with:
69
+ ruby-version: "3.2"
70
+ bundler-cache: true
71
+ - name: Rubocop
72
+ run: bundle exec rubocop
73
+ - name: Documentation correctly written
74
+ run: bundle exec yardoc --no-output --no-save --no-stats --fail-on-warning
@@ -0,0 +1,22 @@
1
+ name: Contributor License Agreement (CLA)
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, synchronize]
6
+ issue_comment:
7
+ types: [created]
8
+
9
+ jobs:
10
+ cla:
11
+ runs-on: ubuntu-latest
12
+ if: |
13
+ (github.event.issue.pull_request
14
+ && !github.event.issue.pull_request.merged_at
15
+ && contains(github.event.comment.body, 'signed')
16
+ )
17
+ || (github.event.pull_request && !github.event.pull_request.merged)
18
+ steps:
19
+ - uses: Shopify/shopify-cla-action@v1
20
+ with:
21
+ github-token: ${{ secrets.GITHUB_TOKEN }}
22
+ cla-token: ${{ secrets.CLA_TOKEN }}
data/.rubocop.yml CHANGED
@@ -2,9 +2,9 @@ inherit_gem:
2
2
  rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.6.5
5
+ TargetRubyVersion: 2.6
6
6
  Exclude:
7
- - 'vendor/bundle/**/*'
7
+ - "vendor/bundle/**/*"
8
8
  Lint/SuppressedException:
9
9
  Exclude:
10
10
  - lib/job-iteration.rb
@@ -16,5 +16,5 @@ Naming/FileName:
16
16
  - lib/job-iteration.rb
17
17
  Style/MethodCallWithArgsParentheses:
18
18
  Exclude:
19
- - 'gemfiles/*'
19
+ - "gemfiles/*"
20
20
  - Gemfile
data/CHANGELOG.md CHANGED
@@ -1,4 +1,26 @@
1
- ### Master (unreleased)
1
+ ### Main (unreleased)
2
+
3
+ Nil
4
+
5
+ ## v1.4.0 (Aug 23, 2023)
6
+
7
+ ### Changes
8
+
9
+ - [338](https://github.com/Shopify/job-iteration/pull/338) - All logs are now `ActiveSupport::Notifications` events and logged using `ActiveSupport::LogSubscriber` to allow customization. Events now always include the `cursor_position` tag.
10
+ - [418](https://github.com/Shopify/job-iteration/pull/418) - Return `nil` from `Iteration#perform`, to signal not to rely on return value.
11
+
12
+ ### Features
13
+
14
+ - [240](https://github.com/Shopify/job-iteration/pull/240) - Allow setting inheritable per-job `job_iteration_max_job_runtime`
15
+ - [310](https://github.com/Shopify/job-iteration/pull/310) - Support nested iteration
16
+ - [341](https://github.com/Shopify/job-iteration/pull/341) - Add `JobIteration.default_retry_backoff`, which sets a default delay when jobs are re-enqueued after being interrupted. Defaults to `nil`, meaning no delay, which matches the current behaviour.
17
+ - [365](https://github.com/Shopify/job-iteration/pull/365) - Support composite primary key as a cursor
18
+
19
+ ### Bug fixes
20
+
21
+ - [289](https://github.com/Shopify/job-iteration/pull/289) - Fix uninitialized constant error when raising `ConditionNotSupportedError` from `ActiveRecordBatchEnumerator`
22
+ - [346](https://github.com/Shopify/job-iteration/pull/346) - Include failed jobs in `total_time`
23
+ - [417](https://github.com/Shopify/job-iteration/pull/417) - Ensure that numerical values are deserialized as such and not as strings.
2
24
 
3
25
  ## v1.3.6 (Mar 9, 2022)
4
26
 
data/Gemfile CHANGED
@@ -15,7 +15,6 @@ gem "mysql2", github: "brianmario/mysql2"
15
15
  gem "globalid"
16
16
  gem "i18n"
17
17
  gem "redis"
18
- gem "database_cleaner"
19
18
 
20
19
  gem "pry"
21
20
  gem "mocha"
data/Gemfile.lock CHANGED
@@ -1,117 +1,114 @@
1
1
  GIT
2
2
  remote: https://github.com/brianmario/mysql2
3
- revision: 8193dc412c6a266045b9d13a9da36c16750939a4
3
+ revision: 79f78f940685396f1b5f30ec502544bb7e3ba9cf
4
4
  specs:
5
- mysql2 (0.5.3)
5
+ mysql2 (0.5.5)
6
6
 
7
7
  PATH
8
8
  remote: .
9
9
  specs:
10
- job-iteration (1.3.6)
10
+ job-iteration (1.4.0)
11
11
  activejob (>= 5.2)
12
12
 
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activejob (6.1.4.6)
17
- activesupport (= 6.1.4.6)
16
+ activejob (7.0.7)
17
+ activesupport (= 7.0.7)
18
18
  globalid (>= 0.3.6)
19
- activemodel (6.1.4.6)
20
- activesupport (= 6.1.4.6)
21
- activerecord (6.1.4.6)
22
- activemodel (= 6.1.4.6)
23
- activesupport (= 6.1.4.6)
24
- activesupport (6.1.4.6)
19
+ activemodel (7.0.7)
20
+ activesupport (= 7.0.7)
21
+ activerecord (7.0.7)
22
+ activemodel (= 7.0.7)
23
+ activesupport (= 7.0.7)
24
+ activesupport (7.0.7)
25
25
  concurrent-ruby (~> 1.0, >= 1.0.2)
26
26
  i18n (>= 1.6, < 2)
27
27
  minitest (>= 5.1)
28
28
  tzinfo (~> 2.0)
29
- zeitwerk (~> 2.3)
30
29
  ast (2.4.2)
31
30
  coderay (1.1.3)
32
- concurrent-ruby (1.1.9)
33
- connection_pool (2.2.5)
34
- database_cleaner (2.0.1)
35
- database_cleaner-active_record (~> 2.0.0)
36
- database_cleaner-active_record (2.0.1)
37
- activerecord (>= 5.a)
38
- database_cleaner-core (~> 2.0.0)
39
- database_cleaner-core (2.0.1)
40
- globalid (1.0.0)
31
+ concurrent-ruby (1.2.2)
32
+ connection_pool (2.4.1)
33
+ globalid (1.1.0)
41
34
  activesupport (>= 5.0)
42
- i18n (1.10.0)
35
+ i18n (1.14.1)
43
36
  concurrent-ruby (~> 1.0)
37
+ json (2.6.3)
38
+ language_server-protocol (3.17.0.3)
44
39
  method_source (1.0.0)
45
- minitest (5.15.0)
46
- mocha (1.13.0)
47
- mono_logger (1.1.1)
40
+ minitest (5.19.0)
41
+ mocha (2.1.0)
42
+ ruby2_keywords (>= 0.0.5)
43
+ mono_logger (1.1.2)
48
44
  multi_json (1.15.0)
49
- mustermann (1.1.1)
45
+ mustermann (3.0.0)
50
46
  ruby2_keywords (~> 0.0.1)
51
- parallel (1.21.0)
52
- parser (3.1.1.0)
47
+ parallel (1.23.0)
48
+ parser (3.2.2.3)
53
49
  ast (~> 2.4.1)
54
- pry (0.14.1)
50
+ racc
51
+ pry (0.14.2)
55
52
  coderay (~> 1.1)
56
53
  method_source (~> 1.0)
57
- rack (2.2.3)
58
- rack-protection (2.1.0)
59
- rack
54
+ racc (1.7.1)
55
+ rack (2.2.8)
56
+ rack-protection (3.1.0)
57
+ rack (~> 2.2, >= 2.2.4)
60
58
  rainbow (3.1.1)
61
59
  rake (13.0.6)
62
- redis (4.6.0)
63
- redis-namespace (1.8.1)
64
- redis (>= 3.0.4)
65
- regexp_parser (2.2.1)
66
- resque (2.2.0)
60
+ redis (5.0.7)
61
+ redis-client (>= 0.9.0)
62
+ redis-client (0.16.0)
63
+ connection_pool
64
+ redis-namespace (1.11.0)
65
+ redis (>= 4)
66
+ regexp_parser (2.8.1)
67
+ resque (2.6.0)
67
68
  mono_logger (~> 1.0)
68
69
  multi_json (~> 1.0)
69
70
  redis-namespace (~> 1.6)
70
71
  sinatra (>= 0.9.2)
71
- vegas (~> 0.1.2)
72
72
  rexml (3.2.5)
73
- rubocop (1.25.1)
73
+ rubocop (1.54.2)
74
+ json (~> 2.3)
75
+ language_server-protocol (>= 3.17.0)
74
76
  parallel (~> 1.10)
75
- parser (>= 3.1.0.0)
77
+ parser (>= 3.2.2.3)
76
78
  rainbow (>= 2.2.2, < 4.0)
77
79
  regexp_parser (>= 1.8, < 3.0)
78
- rexml
79
- rubocop-ast (>= 1.15.1, < 2.0)
80
+ rexml (>= 3.2.5, < 4.0)
81
+ rubocop-ast (>= 1.28.0, < 2.0)
80
82
  ruby-progressbar (~> 1.7)
81
- unicode-display_width (>= 1.4.0, < 3.0)
82
- rubocop-ast (1.16.0)
83
- parser (>= 3.1.1.0)
84
- rubocop-shopify (2.5.0)
85
- rubocop (~> 1.25)
86
- ruby-progressbar (1.11.0)
83
+ unicode-display_width (>= 2.4.0, < 3.0)
84
+ rubocop-ast (1.29.0)
85
+ parser (>= 3.2.1.0)
86
+ rubocop-shopify (2.14.0)
87
+ rubocop (~> 1.51)
88
+ ruby-progressbar (1.13.0)
87
89
  ruby2_keywords (0.0.5)
88
- sidekiq (6.4.1)
89
- connection_pool (>= 2.2.2)
90
- rack (~> 2.0)
91
- redis (>= 4.2.0)
92
- sinatra (2.1.0)
93
- mustermann (~> 1.0)
94
- rack (~> 2.2)
95
- rack-protection (= 2.1.0)
90
+ sidekiq (7.1.2)
91
+ concurrent-ruby (< 2)
92
+ connection_pool (>= 2.3.0)
93
+ rack (>= 2.2.4)
94
+ redis-client (>= 0.14.0)
95
+ sinatra (3.1.0)
96
+ mustermann (~> 3.0)
97
+ rack (~> 2.2, >= 2.2.4)
98
+ rack-protection (= 3.1.0)
96
99
  tilt (~> 2.0)
97
- sorbet-runtime (0.5.9724)
98
- tilt (2.0.10)
99
- tzinfo (2.0.4)
100
+ sorbet-runtime (0.5.10978)
101
+ tilt (2.2.0)
102
+ tzinfo (2.0.6)
100
103
  concurrent-ruby (~> 1.0)
101
- unicode-display_width (2.1.0)
102
- vegas (0.1.11)
103
- rack (>= 1.0.0)
104
- webrick (1.7.0)
105
- yard (0.9.27)
106
- webrick (~> 1.7.0)
107
- zeitwerk (2.5.4)
104
+ unicode-display_width (2.4.2)
105
+ yard (0.9.34)
108
106
 
109
107
  PLATFORMS
110
108
  ruby
111
109
 
112
110
  DEPENDENCIES
113
111
  activerecord
114
- database_cleaner
115
112
  globalid
116
113
  i18n
117
114
  job-iteration!
data/README.md CHANGED
@@ -69,17 +69,15 @@ class BatchesJob < ApplicationJob
69
69
 
70
70
  def build_enumerator(product_id, cursor:)
71
71
  enumerator_builder.active_record_on_batches(
72
- Product.find(product_id).comments,
72
+ Comment.where(product_id: product_id).select(:id),
73
73
  cursor: cursor,
74
74
  batch_size: 100,
75
75
  )
76
76
  end
77
77
 
78
78
  def each_iteration(batch_of_comments, product_id)
79
- # batch_of_comments will contain batches of 100 records
80
- batch_of_comments.each do |comment|
81
- DeleteCommentJob.perform_later(comment)
82
- end
79
+ comment_ids = batch_of_comments.map(&:id)
80
+ CommentService.call(comment_ids: comment_ids)
83
81
  end
84
82
  end
85
83
  ```
@@ -126,17 +124,39 @@ class CsvJob < ApplicationJob
126
124
  enumerator_builder.csv(import.csv, cursor: cursor)
127
125
  end
128
126
 
129
- def each_iteration(csv_row)
127
+ def each_iteration(csv_row, import_id)
130
128
  # insert csv_row to database
131
129
  end
132
130
  end
133
131
  ```
134
132
 
133
+ ```ruby
134
+ class NestedIterationJob < ApplicationJob
135
+ include JobIteration::Iteration
136
+
137
+ def build_enumerator(cursor:)
138
+ enumerator_builder.nested(
139
+ [
140
+ ->(cursor) { enumerator_builder.active_record_on_records(Shop.all, cursor: cursor) },
141
+ ->(shop, cursor) { enumerator_builder.active_record_on_records(shop.products, cursor: cursor) },
142
+ ->(_shop, product, cursor) { enumerator_builder.active_record_on_batch_relations(product.product_variants, cursor: cursor) }
143
+ ],
144
+ cursor: cursor
145
+ )
146
+ end
147
+
148
+ def each_iteration(product_variants_relation)
149
+ # do something
150
+ end
151
+ end
152
+ ```
153
+
135
154
  Iteration hooks into Sidekiq and Resque out of the box to support graceful interruption. No extra configuration is required.
136
155
 
137
156
  ## Guides
138
157
 
139
158
  * [Iteration: how it works](guides/iteration-how-it-works.md)
159
+ * [Job argument semantics](guides/argument-semantics.md)
140
160
  * [Best practices](guides/best-practices.md)
141
161
  * [Writing custom enumerator](guides/custom-enumerator.md)
142
162
  * [Throttling](guides/throttling.md)
@@ -171,8 +191,6 @@ There a few configuration assumptions that are required for Iteration to work wi
171
191
 
172
192
  **Why is it important that `each_iteration` takes less than 30 seconds?** When the job worker is scheduled for restart or shutdown, it gets a notice to finish remaining unit of work. To guarantee that no progress is lost we need to make sure that `each_iteration` completes within a reasonable amount of time.
173
193
 
174
- **What do I do if each iteration takes a long time, because it's doing nested operations?** If your `each_iteration` is complex, we recommend enqueuing another job, which will run your nested business logic. We may expose primitives in the future to do this more effectively, but this is not terribly common today.
175
-
176
194
  **Why do I use have to use this ugly helper in `build_enumerator`? Why can't you automatically infer it?** This is how the first version of the API worked. We checked the type of object returned by `build_enumerable`, and whether it was ActiveRecord Relation or an Array, we used the matching adapter. This caused opaque type branching in Iteration internals and it didn’t allow developers to craft their own Enumerators and control the cursor value. We made a decision to _always_ return Enumerator instance from `build_enumerator`. Now we provide explicit helpers to convert ActiveRecord Relation or an Array to Enumerator, and for more complex iteration flows developers can build their own `Enumerator` objects.
177
195
 
178
196
  **What is the difference between Enumerable and Enumerator?** We recomend [this post](http://blog.arkency.com/2014/01/ruby-to-enum-for-enumerator/) to learn more about Enumerators in Ruby.
data/dev.yml CHANGED
@@ -7,8 +7,8 @@ up:
7
7
  - mysql-client:
8
8
  or: [mysql@5.7]
9
9
  - ruby:
10
- version: 2.6.5
11
- - railgun
10
+ version: 2.7.6
11
+ - isogun
12
12
  - bundler
13
13
  - custom:
14
14
  name: Create Job Iteration database
@@ -0,0 +1,128 @@
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
+ ```