job-iteration 1.1.3 → 1.1.4

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: 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