job-iteration 1.6.0 → 1.8.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: 55bbe81f1fe209219d6dbb5aa469f027834d6b09dbdd6485b305fb4e3aa9bcb1
4
- data.tar.gz: cf611985f1b8b6dc2cb1d00026fb53d6716ff353d0e59a96235f0e8538e1878b
3
+ metadata.gz: a38399b1ad6f7c81b2dd731e39abc75b4febcbac50ea9c310c6c96ada5f802ff
4
+ data.tar.gz: 96182f4dcdd4594c9fde855b2394b96a516e7134671a18905ebd3ae6b13ddcfa
5
5
  SHA512:
6
- metadata.gz: e8857b87a8a999ec88b584896ff7c384310badbf7c2e7c553b9b64fed108cb7756fdc365a0008766d9179c40279ed221c96eca0b363fd5283f6c7d3d676ab82f
7
- data.tar.gz: 89e9e6d584fef095c6c04b96a369abbf710283dffe9856091eef3fc7492222bd7591f22a8ebaaa7ea76f5d3281cd2c8163ba57d37ead43b095fb08fb377aea3a
6
+ metadata.gz: 515d1e0106c1b7c20ce3058df4df1c0c2a29dfb0ebe297bc3ac81da5365b7c8d68cf1058dc5de9c1c37331570260065e59aa86b1801f3b821a70d504aa3b25ac
7
+ data.tar.gz: 27303a6ea6910734d7d23f6ef0570cf7b2e6f011b1776731829e8366d93e2ad1032c1fce4bf44dee42e1c253f209f97cda8c761824ab7b81f8bfa51a339b0218
@@ -15,29 +15,37 @@ jobs:
15
15
  strategy:
16
16
  matrix:
17
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"]
18
+ rails: ["5.2", "6.0", "6.1", "7.0", "7.1", "7.2", "edge"]
19
19
  gemfile: [rails_gems]
20
20
  exclude:
21
21
  - ruby: "2.6"
22
22
  rails: "7.0"
23
23
  - ruby: "2.6"
24
24
  rails: "7.1"
25
+ - ruby: "2.6"
26
+ rails: "7.2"
25
27
  - ruby: "2.6"
26
28
  rails: "edge"
27
29
  - ruby: "2.7"
28
30
  rails: "7.1"
31
+ - ruby: "2.7"
32
+ rails: "7.2"
29
33
  - ruby: "2.7"
30
34
  rails: "edge"
31
35
  - ruby: "3.0"
32
36
  rails: "5.2"
33
37
  - ruby: "3.0"
34
38
  rails: "7.1"
39
+ - ruby: "3.0"
40
+ rails: "7.2"
35
41
  - ruby: "3.0"
36
42
  rails: "edge"
37
43
  - ruby: "3.1"
38
44
  rails: "5.2"
39
45
  - ruby: "3.1"
40
46
  rails: "6.0"
47
+ - ruby: "3.1"
48
+ rails: "edge"
41
49
  - ruby: "3.2"
42
50
  rails: "5.2"
43
51
  - ruby: "3.2"
data/CHANGELOG.md CHANGED
@@ -1,6 +1,38 @@
1
1
  ### Main (unreleased)
2
2
 
3
- Nil
3
+ ### Changes
4
+
5
+ nil
6
+
7
+ ### Features
8
+
9
+ nil
10
+
11
+ ### Bug fixes
12
+
13
+ nil
14
+
15
+ ## v1.8.0 (Dec 10, 2024)
16
+
17
+ ### Changes
18
+
19
+ - [513](https://github.com/Shopify/job-iteration/pull/513) Deprecate returning enumerators from `build_enumerator` that are not wrapped with `enumerator_builder.wrap`. The built-in enumerator builders now always wrap.
20
+
21
+ ### Features
22
+
23
+ - [340](https://github.com/Shopify/job-iteration/pull/340) Add `cursor.iteration` instrumentation for the query to fetch the next batch of records for the Active Record cursor.
24
+ - [523](https://github.com/Shopify/job-iteration/pull/523) Add interruption adapter for [aws-activejob-sqs](https://github.com/aws/aws-activejob-sqs-ruby).
25
+
26
+ ### Bug fixes
27
+
28
+ - [515](https://github.com/Shopify/job-iteration/pull/515) Fix size of array enumerators.
29
+
30
+ ## v1.7.0 (Oct 11, 2024)
31
+
32
+ ### Features
33
+
34
+ - [509](https://github.com/Shopify/job-iteration/pull/509) - Added CSV batching functionality to `EnumeratorBuilder` with `build_csv_enumerator_on_batches` method and `csv_on_batches` alias.
35
+ - [512](https://github.com/Shopify/job-iteration/pull/512) - Added support for custom timezones on `ActiveRecordEnumerator` and `ActiveRecordBatchEnumerator`. This allows for using cursors with datetime columns where `ActiveRecord.default_timezone` is set to `:local` and Active Record is not using the same timezone as the database.
4
36
 
5
37
  ## v1.6.0 (Sep 24, 2024)
6
38
 
@@ -10,9 +42,13 @@ Nil
10
42
  - [505](https://github.com/Shopify/job-iteration/pull/505) - Add interruption adapter for [Solid Queue](https://github.com/rails/solid_queue).
11
43
 
12
44
  ## v1.5.1 (May 29,2024)
45
+
46
+ ### Bug fixes
47
+
13
48
  - [483](https://github.com/Shopify/job-iteration/pull/483) - Reverts [#456 Use Arel instead of String for AR Enumerator conditionals](https://github.com/Shopify/job-iteration/pull/456)
14
49
 
15
50
  ## v1.5.0 (May 29, 2024)
51
+
16
52
  ### Changes
17
53
 
18
54
  - [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.
@@ -29,7 +65,7 @@ when generating position for cursor based on `:id` column (Rails 7.1 and above,
29
65
  primary models are now supported). This ensures we grab the value of the id column, rather than a
30
66
  potentially composite primary key value.
31
67
  - [456](https://github.com/Shopify/job-iteration/pull/431) - Use Arel to generate SQL that's type compatible for the
32
- cursor pagination conditionals in ActiveRecord cursor. Previously, the cursor would coerce numeric ids to a string value
68
+ cursor pagination conditionals in ActiveRecord cursor. Previously, the cursor would coerce numeric ids to a string value
33
69
  (e.g.: `... AND id > '1'`)
34
70
 
35
71
  ## v1.4.1 (Sep 5, 2023)
data/Gemfile.lock CHANGED
@@ -1,6 +1,6 @@
1
1
  GIT
2
2
  remote: https://github.com/brianmario/mysql2
3
- revision: f6a9b68b42a51d1a370403f11eb88527dcb42dc6
3
+ revision: 58f8d009a0d107776dd6d24c9906426fe0f7b856
4
4
  specs:
5
5
  mysql2 (0.5.6)
6
6
  bigdecimal
@@ -8,7 +8,7 @@ GIT
8
8
  PATH
9
9
  remote: .
10
10
  specs:
11
- job-iteration (1.6.0)
11
+ job-iteration (1.8.0)
12
12
  activejob (>= 5.2)
13
13
 
14
14
  GEM
@@ -49,7 +49,7 @@ GEM
49
49
  language_server-protocol (3.17.0.3)
50
50
  method_source (1.1.0)
51
51
  minitest (5.24.0)
52
- mocha (2.4.5)
52
+ mocha (2.7.0)
53
53
  ruby2_keywords (>= 0.0.5)
54
54
  mono_logger (1.1.2)
55
55
  multi_json (1.15.0)
@@ -60,7 +60,7 @@ GEM
60
60
  parser (3.3.3.0)
61
61
  ast (~> 2.4.1)
62
62
  racc
63
- pry (0.14.2)
63
+ pry (0.15.0)
64
64
  coderay (~> 1.1)
65
65
  method_source (~> 1.0)
66
66
  racc (1.8.0)
@@ -84,8 +84,7 @@ GEM
84
84
  multi_json (~> 1.0)
85
85
  redis-namespace (~> 1.6)
86
86
  sinatra (>= 0.9.2)
87
- rexml (3.3.6)
88
- strscan
87
+ rexml (3.3.9)
89
88
  rubocop (1.64.1)
90
89
  json (~> 2.3)
91
90
  language_server-protocol (>= 3.17.0)
@@ -115,7 +114,6 @@ GEM
115
114
  rack-session (>= 2.0.0, < 3)
116
115
  tilt (~> 2.0)
117
116
  sorbet-runtime (0.5.11460)
118
- strscan (3.1.0)
119
117
  tilt (2.4.0)
120
118
  timeout (0.4.1)
121
119
  tzinfo (2.0.6)
data/README.md CHANGED
@@ -161,7 +161,7 @@ Iteration hooks into Sidekiq and Resque out of the box to support graceful inter
161
161
  * [Writing custom enumerator](guides/custom-enumerator.md)
162
162
  * [Throttling](guides/throttling.md)
163
163
 
164
- For more detailed documentation, see [rubydoc](https://www.rubydoc.info/github/Shopify/job-iteration).
164
+ For more detailed documentation, see [rubydoc](https://www.rubydoc.info/gems/job-iteration).
165
165
 
166
166
  ## Requirements
167
167
 
@@ -78,12 +78,14 @@ class LoadRefundsForChargeJob < ActiveJob::Base
78
78
  # Use an exponential back-off strategy when Stripe's API returns errors.
79
79
 
80
80
  def build_enumerator(charge_id, cursor:)
81
- StripeListEnumerator.new(
82
- Stripe::Refund,
83
- params: { charge: charge_id}, # "charge_id" will be a prefixed Stripe ID such as "chrg_123"
84
- options: { api_key: "sk_test_123", stripe_version: "2018-01-18" },
85
- cursor: cursor
86
- ).to_enumerator
81
+ enumerator_builder.wrap(
82
+ StripeListEnumerator.new(
83
+ Stripe::Refund,
84
+ params: { charge: charge_id}, # "charge_id" will be a prefixed Stripe ID such as "chrg_123"
85
+ options: { api_key: "sk_test_123", stripe_version: "2018-01-18" },
86
+ cursor: cursor
87
+ ).to_enumerator
88
+ )
87
89
  end
88
90
 
89
91
  # Note that in this case `each_iteration` will only receive one positional argument per iteration.
@@ -114,9 +116,11 @@ class RedisPopListJob < ActiveJob::Base
114
116
  # @see https://redis.io/commands/lpop/
115
117
  def build_enumerator(*)
116
118
  @redis = Redis.new
117
- Enumerator.new do |yielder|
118
- yielder.yield @redis.lpop(key), nil
119
- end
119
+ enumerator_builder.wrap(
120
+ Enumerator.new do |yielder|
121
+ yielder.yield @redis.lpop(key), nil
122
+ end
123
+ )
120
124
  end
121
125
 
122
126
  def each_iteration(item_from_redis)
data/guides/throttling.md CHANGED
@@ -38,9 +38,31 @@ def build_enumerator(_params, cursor:)
38
38
  end
39
39
  ```
40
40
 
41
+ If you want to apply throttling on all jobs, you can subclass your own EnumeratorBuilder and override the default
42
+ enumerator builder. The builder always wraps the returned enumerators from `build_enumerator`
43
+
44
+ ```ruby
45
+ class MyOwnBuilder < JobIteration::EnumeratorBuilder
46
+ class Wrapper < Enumerator
47
+ class << self
48
+ def wrap(_builder, enum)
49
+ ThrottleEnumerator.new(
50
+ enum,
51
+ nil,
52
+ throttle_on: -> { DatabaseStatus.unhealthy? },
53
+ backoff: 30.seconds
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ JobIteration.enumerator_builder = MyOwnBuilder
61
+ ```
62
+
41
63
  Note that it's up to you to implement `DatabaseStatus.unhealthy?` that works for your database choice. At Shopify, a helper like `DatabaseStatus` checks the following MySQL metrics:
42
64
 
43
65
  * Replication lag across all regions
44
66
  * DB threads
45
67
  * DB is available for writes (otherwise indicates a failover happening)
46
- * [Semian](https://github.com/shopify/semian) open circuits
68
+ * [Semian](https://github.com/shopify/semian) open circuits
@@ -8,8 +8,9 @@ module JobIteration
8
8
 
9
9
  SQL_DATETIME_WITH_NSEC = "%Y-%m-%d %H:%M:%S.%N"
10
10
 
11
- def initialize(relation, columns: nil, batch_size: 100, cursor: nil)
11
+ def initialize(relation, columns: nil, batch_size: 100, timezone: nil, cursor: nil)
12
12
  @batch_size = batch_size
13
+ @timezone = timezone
13
14
  @primary_key = "#{relation.table_name}.#{relation.primary_key}"
14
15
  @columns = Array(columns&.map(&:to_s) || @primary_key)
15
16
  @primary_key_index = @columns.index(@primary_key) || @columns.index(relation.primary_key)
@@ -114,7 +115,10 @@ module JobIteration
114
115
  end
115
116
 
116
117
  def column_value(value)
117
- value.is_a?(Time) ? value.strftime(SQL_DATETIME_WITH_NSEC) : value
118
+ return value unless value.is_a?(Time)
119
+
120
+ value = value.in_time_zone(@timezone) unless @timezone.nil?
121
+ value.strftime(SQL_DATETIME_WITH_NSEC)
118
122
  end
119
123
  end
120
124
  end
@@ -7,9 +7,10 @@ module JobIteration
7
7
  class ActiveRecordEnumerator
8
8
  SQL_DATETIME_WITH_NSEC = "%Y-%m-%d %H:%M:%S.%N"
9
9
 
10
- def initialize(relation, columns: nil, batch_size: 100, cursor: nil)
10
+ def initialize(relation, columns: nil, batch_size: 100, timezone: nil, cursor: nil)
11
11
  @relation = relation
12
12
  @batch_size = batch_size
13
+ @timezone = timezone
13
14
  @columns = if columns
14
15
  Array(columns)
15
16
  else
@@ -31,7 +32,7 @@ module JobIteration
31
32
  def batches
32
33
  cursor = finder_cursor
33
34
  Enumerator.new(method(:size)) do |yielder|
34
- while (records = cursor.next_batch(@batch_size))
35
+ while (records = instrument_next_batch(cursor))
35
36
  yielder.yield(records, cursor_value(records.last)) if records.any?
36
37
  end
37
38
  end
@@ -43,6 +44,12 @@ module JobIteration
43
44
 
44
45
  private
45
46
 
47
+ def instrument_next_batch(cursor)
48
+ ActiveSupport::Notifications.instrument("active_record_cursor.iteration") do
49
+ cursor.next_batch(@batch_size)
50
+ end
51
+ end
52
+
46
53
  def cursor_value(record)
47
54
  positions = @columns.map do |column|
48
55
  attribute_name = column.to_s.split(".").last
@@ -61,6 +68,7 @@ module JobIteration
61
68
  value = record.read_attribute(attribute.to_sym)
62
69
  case record.class.columns_hash.fetch(attribute).type
63
70
  when :datetime
71
+ value = value.in_time_zone(@timezone) unless @timezone.nil?
64
72
  value.strftime(SQL_DATETIME_WITH_NSEC)
65
73
  else
66
74
  value
@@ -16,8 +16,7 @@ module JobIteration
16
16
  # `enumerator_builder` is _always_ the type that is returned from
17
17
  # `build_enumerator`. This prevents people from implementing custom
18
18
  # Enumerators without wrapping them in
19
- # `enumerator_builder.wrap(custom_enum)`. We don't do this yet for backwards
20
- # compatibility with raw calls to EnumeratorBuilder. Think of these wrappers
19
+ # `enumerator_builder.wrap(custom_enum)`. Think of these wrappers
21
20
  # the way you should a middleware.
22
21
  class Wrapper < Enumerator
23
22
  class << self
@@ -63,7 +62,7 @@ module JobIteration
63
62
  cursor + 1
64
63
  end
65
64
 
66
- wrap(self, enumerable.each_with_index.drop(drop).to_enum { enumerable.size })
65
+ wrap(self, enumerable.each_with_index.drop(drop).to_enum { enumerable.size - drop })
67
66
  end
68
67
 
69
68
  # Builds Enumerator from Active Record Relation. Each Enumerator tick moves the cursor one row forward.
@@ -131,17 +130,24 @@ module JobIteration
131
130
  enum
132
131
  end
133
132
 
134
- def build_throttle_enumerator(enum, throttle_on:, backoff:)
135
- JobIteration::ThrottleEnumerator.new(
136
- enum,
133
+ def build_throttle_enumerator(enumerable, throttle_on:, backoff:)
134
+ enum = JobIteration::ThrottleEnumerator.new(
135
+ enumerable,
137
136
  @job,
138
137
  throttle_on: throttle_on,
139
138
  backoff: backoff,
140
139
  ).to_enum
140
+ wrap(self, enum)
141
141
  end
142
142
 
143
143
  def build_csv_enumerator(enumerable, cursor:)
144
- CsvEnumerator.new(enumerable).rows(cursor: cursor)
144
+ enum = CsvEnumerator.new(enumerable).rows(cursor: cursor)
145
+ wrap(self, enum)
146
+ end
147
+
148
+ def build_csv_enumerator_on_batches(enumerable, cursor:, batch_size: 100)
149
+ enum = CsvEnumerator.new(enumerable).batches(cursor: cursor, batch_size: batch_size)
150
+ wrap(self, enum)
145
151
  end
146
152
 
147
153
  # Builds Enumerator for nested iteration.
@@ -175,7 +181,8 @@ module JobIteration
175
181
  # end
176
182
  #
177
183
  def build_nested_enumerator(enums, cursor:)
178
- NestedEnumerator.new(enums, cursor: cursor).each
184
+ enum = NestedEnumerator.new(enums, cursor: cursor).each
185
+ wrap(self, enum)
179
186
  end
180
187
 
181
188
  alias_method :once, :build_once_enumerator
@@ -186,6 +193,7 @@ module JobIteration
186
193
  alias_method :active_record_on_batch_relations, :build_active_record_enumerator_on_batch_relations
187
194
  alias_method :throttle, :build_throttle_enumerator
188
195
  alias_method :csv, :build_csv_enumerator
196
+ alias_method :csv_on_batches, :build_csv_enumerator_on_batches
189
197
  alias_method :nested, :build_nested_enumerator
190
198
 
191
199
  private
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "aws-activejob-sqs"
5
+ rescue LoadError
6
+ # Aws::ActiveJob::SQS is not available, no need to load the adapter
7
+ return
8
+ end
9
+
10
+ begin
11
+ # Aws::ActiveJob::SQS.on_worker_stop was introduced in Aws::ActiveJob::SQS 0.1.1
12
+ gem("aws-activejob-sqs", ">= 0.1.1")
13
+ rescue Gem::LoadError
14
+ warn("job-iteration's interruption adapter for SQS requires aws-activejob-sqs 0.1.1 or newer")
15
+ return
16
+ end
17
+
18
+ module JobIteration
19
+ module InterruptionAdapters
20
+ module SqsAdapter
21
+ class << self
22
+ attr_accessor :stopping
23
+
24
+ def call
25
+ stopping
26
+ end
27
+ end
28
+
29
+ Aws::ActiveJob::SQS.on_worker_stop do
30
+ SqsAdapter.stopping = true
31
+ end
32
+ end
33
+
34
+ register(:sqs, SqsAdapter)
35
+ end
36
+ end
@@ -4,7 +4,7 @@ require_relative "interruption_adapters/null_adapter"
4
4
 
5
5
  module JobIteration
6
6
  module InterruptionAdapters
7
- BUNDLED_ADAPTERS = [:good_job, :resque, :sidekiq, :solid_queue].freeze # @api private
7
+ BUNDLED_ADAPTERS = [:good_job, :resque, :sidekiq, :solid_queue, :sqs].freeze # @api private
8
8
 
9
9
  class << self
10
10
  # Returns adapter for specified name.
@@ -219,7 +219,14 @@ module JobIteration
219
219
  end
220
220
 
221
221
  def assert_enumerator!(enum)
222
- return if enum.is_a?(Enumerator)
222
+ if enum.is_a?(Enumerator)
223
+ unless enum.is_a?(JobIteration.enumerator_builder::Wrapper)
224
+ JobIteration::Deprecation.warn("Returning an unwrapped enumerator from build_enumerator is deprecated. " \
225
+ "Wrap the enumerator using enumerator_builder.wrap(my_enumerator) instead.")
226
+ end
227
+
228
+ return
229
+ end
223
230
 
224
231
  raise ArgumentError, <<~EOS
225
232
  #build_enumerator is expected to return Enumerator object, but returned #{enum.class}.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JobIteration
4
- VERSION = "1.6.0"
4
+ VERSION = "1.8.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job-iteration
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-25 00:00:00.000000000 Z
11
+ date: 2024-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -67,6 +67,7 @@ files:
67
67
  - lib/job-iteration/interruption_adapters/resque_adapter.rb
68
68
  - lib/job-iteration/interruption_adapters/sidekiq_adapter.rb
69
69
  - lib/job-iteration/interruption_adapters/solid_queue_adapter.rb
70
+ - lib/job-iteration/interruption_adapters/sqs_adapter.rb
70
71
  - lib/job-iteration/iteration.rb
71
72
  - lib/job-iteration/log_subscriber.rb
72
73
  - lib/job-iteration/nested_enumerator.rb
@@ -95,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
96
  - !ruby/object:Gem::Version
96
97
  version: '0'
97
98
  requirements: []
98
- rubygems_version: 3.5.18
99
+ rubygems_version: 3.5.23
99
100
  signing_key:
100
101
  specification_version: 4
101
102
  summary: Makes your background jobs interruptible and resumable.