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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -2
- data/README.md +1 -0
- data/guides/throttling.md +46 -0
- data/lib/job-iteration/enumerator_builder.rb +11 -10
- data/lib/job-iteration/throttle_enumerator.rb +46 -0
- data/lib/job-iteration/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21dc74805b967b6082859b0c9a07d955f9f77ca3e82f250ceaf4f2aecf4afd7b
|
4
|
+
data.tar.gz: 23ea1a3e03c8b0578dcf852449204f60e5391bdb92f26987da7d42f45b052595
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b3b5b27d999f8ea00a443d01857f0483fef67edcd0b0525d8ef8003c8814b9efe1e34713d6f45d0aa2de68c79c2517be141595f37c2501265f04ea6a4da7e0af
|
7
|
+
data.tar.gz: e47797081fc96034f2fe21746528047cba4f2873a0e6a36a7a38f2265d38a1dcb045f66216c3e9579699d63a9b6416136cf456e630404bb6d5381c433ef7232c
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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.
|
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
|
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.
|
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-
|
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
|