job-iteration 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3321e73fd83420a90163ec8963f381897cd1bd6e06717ce3c1e5db645e3aa823
4
- data.tar.gz: bae3d1af0d3d803423c42c0085d3b5c9487c804e7362a9a70fe845e6d1abc406
3
+ metadata.gz: 21dc74805b967b6082859b0c9a07d955f9f77ca3e82f250ceaf4f2aecf4afd7b
4
+ data.tar.gz: 23ea1a3e03c8b0578dcf852449204f60e5391bdb92f26987da7d42f45b052595
5
5
  SHA512:
6
- metadata.gz: f78e4bbc18af7343762de3dbfbb4fc958512b7e62806a8adb372f5d94d3fa37b2e16fa0228f343e37679516ddb477099b8d9c3f4517493eb9fb9594ffba86e9e
7
- data.tar.gz: d1593c8350cf85a78754375f808fd467421a9b04349fb776b3e7471a4ab12d3df14e104021c17ffc80f9595248b2e99659a6747954296dd0d15afefa244bc4ec
6
+ metadata.gz: b3b5b27d999f8ea00a443d01857f0483fef67edcd0b0525d8ef8003c8814b9efe1e34713d6f45d0aa2de68c79c2517be141595f37c2501265f04ea6a4da7e0af
7
+ data.tar.gz: e47797081fc96034f2fe21746528047cba4f2873a0e6a36a7a38f2265d38a1dcb045f66216c3e9579699d63a9b6416136cf456e630404bb6d5381c433ef7232c
@@ -5,7 +5,7 @@ AllCops:
5
5
  TargetRubyVersion: 2.4.4
6
6
  Exclude:
7
7
  - 'vendor/bundle/**/*'
8
- Lint/HandleExceptions:
8
+ Lint/SuppressedException:
9
9
  Exclude:
10
10
  - lib/job-iteration.rb
11
11
  Style/GlobalVars:
@@ -4,6 +4,11 @@
4
4
 
5
5
  #### Bug fix
6
6
 
7
+ ## v1.1.4 (December 13, 2019)
8
+
9
+ - [45](https://github.com/Shopify/job-iteration/pull/45) - Add Throttle enumerator
10
+
11
+
7
12
  ### v1.1.3 (August 20, 2019)
8
13
 
9
14
  - [36](https://github.com/shopify/job-iteration/pull/39) - Check method validation at job initialization step
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ gemspec
11
11
  gem 'sidekiq'
12
12
  gem 'resque'
13
13
 
14
- gem 'mysql2', '~> 0.4.4'
14
+ gem 'mysql2', '~> 0.5'
15
15
  gem 'globalid'
16
16
  gem 'i18n'
17
17
  gem 'redis'
@@ -20,6 +20,6 @@ gem 'database_cleaner'
20
20
  gem 'pry'
21
21
  gem 'mocha'
22
22
 
23
- gem 'rubocop'
23
+ gem 'rubocop', '~> 0.77.0'
24
24
  gem 'yard'
25
25
  gem 'rake'
data/README.md CHANGED
@@ -112,6 +112,7 @@ Iteration hooks into Sidekiq and Resque out of the box to support graceful inter
112
112
  * [Iteration: how it works](guides/iteration-how-it-works.md)
113
113
  * [Best practices](guides/best-practices.md)
114
114
  * [Writing custom enumerator](guides/custom-enumerator.md)
115
+ * [Throttling](guides/throttling.md)
115
116
 
116
117
  For more detailed documentation, see [rubydoc](https://www.rubydoc.info/github/Shopify/job-iteration).
117
118
 
@@ -0,0 +1,46 @@
1
+ Iteration comes with a special wrapper enumerator that allows you to throttle iterations based on external signal (e.g. database health).
2
+
3
+ Consider this example:
4
+
5
+ ```ruby
6
+ class InactiveAccountDeleteJob < ActiveJob::Base
7
+ include JobIteration::Iteration
8
+
9
+ def build_enumerator(_params, cursor:)
10
+ enumerator_builder.active_record_on_batches(
11
+ Account.inactive,
12
+ cursor: cursor
13
+ )
14
+ end
15
+
16
+ def each_iteration(batch, _params)
17
+ Account.where(id: batch.map(&:id)).delete_all
18
+ end
19
+ end
20
+ ```
21
+
22
+ For an app that keeps track of customer accounts, it's typical to purge old data that's no longer relevant for storage.
23
+
24
+ At the same time, if you've got a lot of DB writes to perform, this can cause extra load on the database and slow down other parts of your service.
25
+
26
+ You can change `build_enumerator` to wrap enumeration on DB rows into a throttle enumerator, which takes signal as a proc and enqueues the job for later in case the proc returned `true`.
27
+
28
+ ```ruby
29
+ def build_enumerator(_params, cursor:)
30
+ enumerator_builder.build_throttle_enumerator(
31
+ enumerator_builder.active_record_on_batches(
32
+ Account.inactive,
33
+ cursor: cursor
34
+ ),
35
+ throttle_on: -> { DatabaseStatus.unhealthy? },
36
+ backoff: 30.seconds
37
+ )
38
+ end
39
+ ```
40
+
41
+ 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
+
43
+ * Replication lag across all regions
44
+ * DB threads
45
+ * DB is available for writes (otherwise indicates a failover happening)
46
+ * [Semian](https://github.com/shopify/semian) open circuits
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "./active_record_enumerator"
3
3
  require_relative "./csv_enumerator"
4
+ require_relative "./throttle_enumerator"
4
5
  require "forwardable"
5
6
 
6
7
  module JobIteration
@@ -61,16 +62,6 @@ module JobIteration
61
62
  wrap(self, enumerable.each_with_index.drop(drop).to_enum { enumerable.size })
62
63
  end
63
64
 
64
- # Builds Enumerator from a lock queue instance that belongs to a job.
65
- # The helper is only to be used from jobs that use LockQueue module.
66
- def build_lock_queue_enumerator(lock_queue, at_most_once:)
67
- unless lock_queue.is_a?(BackgroundQueue::LockQueue::RedisQueue) ||
68
- lock_queue.is_a?(BackgroundQueue::LockQueue::RolloutRedisQueue)
69
- raise ArgumentError, "an argument to #build_lock_queue_enumerator must be a LockQueue"
70
- end
71
- wrap(self, BackgroundQueue::LockQueueEnumerator.new(lock_queue, at_most_once: at_most_once).to_enum)
72
- end
73
-
74
65
  # Builds Enumerator from Active Record Relation. Each Enumerator tick moves the cursor one row forward.
75
66
  #
76
67
  # +columns:+ argument is used to build the actual query for iteration. +columns+: defaults to primary key:
@@ -119,11 +110,21 @@ module JobIteration
119
110
  wrap(self, enum)
120
111
  end
121
112
 
113
+ def build_throttle_enumerator(enum, throttle_on:, backoff:)
114
+ JobIteration::ThrottleEnumerator.new(
115
+ enum,
116
+ @job,
117
+ throttle_on: throttle_on,
118
+ backoff: backoff
119
+ ).to_enum
120
+ end
121
+
122
122
  alias_method :once, :build_once_enumerator
123
123
  alias_method :times, :build_times_enumerator
124
124
  alias_method :array, :build_array_enumerator
125
125
  alias_method :active_record_on_records, :build_active_record_enumerator_on_records
126
126
  alias_method :active_record_on_batches, :build_active_record_enumerator_on_batches
127
+ alias_method :throttle, :build_throttle_enumerator
127
128
 
128
129
  private
129
130
 
@@ -0,0 +1,46 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+ module JobIteration
4
+ # ThrottleEnumerator allows you to throttle iterations
5
+ # based on external signal (e.g. database health).
6
+ # @example
7
+ # def build_enumerator(_params, cursor:)
8
+ # enumerator_builder.build_throttle_enumerator(
9
+ # enumerator_builder.active_record_on_batches(
10
+ # Account.inactive,
11
+ # cursor: cursor
12
+ # ),
13
+ # throttle_on: -> { DatabaseStatus.unhealthy? },
14
+ # backoff: 30.seconds
15
+ # )
16
+ # end
17
+ # The enumerator from above will mimic +active_record_on_batches+,
18
+ # except when +DatabaseStatus.unhealthy?+ starts to return true.
19
+ # In that case, it will re-enqueue the job with a specified backoff.
20
+ class ThrottleEnumerator
21
+ def initialize(enum, job, throttle_on:, backoff:)
22
+ @enum = enum
23
+ @job = job
24
+ @throttle_on = throttle_on
25
+ @backoff = backoff
26
+ end
27
+
28
+ def to_enum
29
+ Enumerator.new(-> { @enum.size }) do |yielder|
30
+ @enum.each do |*val|
31
+ if should_throttle?
32
+ ActiveSupport::Notifications.instrument("throttled.iteration", job_class: @job.class.name)
33
+ @job.retry_job(wait: @backoff)
34
+ throw(:abort, :skip_complete_callbacks)
35
+ end
36
+
37
+ yielder.yield(*val)
38
+ end
39
+ end
40
+ end
41
+
42
+ def should_throttle?
43
+ @throttle_on.call
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JobIteration
4
- VERSION = "1.1.3"
4
+ VERSION = "1.1.4"
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.1.3
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-20 00:00:00.000000000 Z
11
+ date: 2019-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -62,6 +62,7 @@ files:
62
62
  - guides/best-practices.md
63
63
  - guides/custom-enumerator.md
64
64
  - guides/iteration-how-it-works.md
65
+ - guides/throttling.md
65
66
  - job-iteration.gemspec
66
67
  - lib/job-iteration.rb
67
68
  - lib/job-iteration/active_record_cursor.rb
@@ -72,6 +73,7 @@ files:
72
73
  - lib/job-iteration/integrations/sidekiq.rb
73
74
  - lib/job-iteration/iteration.rb
74
75
  - lib/job-iteration/test_helper.rb
76
+ - lib/job-iteration/throttle_enumerator.rb
75
77
  - lib/job-iteration/version.rb
76
78
  - railgun.yml
77
79
  homepage: https://github.com/shopify/job-iteration