record_store 6.1.1 → 6.3.1
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-https---shopify-github-io-ruby-style-guide-rubocop-yml +2 -2
- data/.travis.yml +1 -1
- data/CHANGELOG.md +15 -0
- data/README.md +4 -0
- data/dev.yml +1 -1
- data/lib/record_store/provider.rb +44 -0
- data/lib/record_store/provider/dnsimple.rb +17 -9
- data/lib/record_store/provider/dnsimple/patch_api_header.rb +3 -3
- data/lib/record_store/provider/ns1.rb +7 -5
- data/lib/record_store/provider/ns1/client.rb +21 -8
- data/lib/record_store/provider/ns1/patch_api_header.rb +29 -13
- data/lib/record_store/provider/provider_utils/waiter.rb +41 -0
- data/lib/record_store/record.rb +4 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +30 -2
- data/lib/record_store/zone/config.rb +4 -0
- data/record_store.gemspec +1 -1
- metadata +7 -7
- data/lib/record_store/provider/provider_utils/rate_limit.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6875c27f0cf86510b733a58f668d44560eeb40e7c8b6c9438183fee175bc514
|
4
|
+
data.tar.gz: 3599cc9903a5afe46d544f8aeef349961fdc2d95011973325bbdcc75972c56c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a6def9342919457cc71fdda4691c92310450dfc28a476eee6d353b73798b8d1b612b4775982704b0c44b2e657149a02f0e96330796b03d591619ad5930509ae
|
7
|
+
data.tar.gz: 42183ae0ecd31e27a2d40977ecdda6cb51c0368ff24ac3a2de4411ab016809c5551ee3faeb50eb53503be059ece4bc41889e6c73d3e1f0ab53e7ab9290563628
|
@@ -671,7 +671,7 @@ Style/LineEndConcatenation:
|
|
671
671
|
Style/MethodCallWithoutArgsParentheses:
|
672
672
|
Enabled: true
|
673
673
|
|
674
|
-
|
674
|
+
Lint/MissingSuper:
|
675
675
|
Enabled: true
|
676
676
|
|
677
677
|
Style/MissingRespondToMissing:
|
@@ -964,7 +964,7 @@ Lint/UselessAccessModifier:
|
|
964
964
|
Lint/UselessAssignment:
|
965
965
|
Enabled: true
|
966
966
|
|
967
|
-
Lint/
|
967
|
+
Lint/BinaryOperatorWithIdenticalOperands:
|
968
968
|
Enabled: true
|
969
969
|
|
970
970
|
Lint/UselessElseWithoutRescue:
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 6.3.1
|
4
|
+
- Improve resiliency in the face of temporary provider outages [BUGFIX]
|
5
|
+
|
6
|
+
## 6.3.0
|
7
|
+
- Support for configurable number of threads via environment variable [FEATURE]
|
8
|
+
|
9
|
+
## 6.2.1
|
10
|
+
- Improved error reporting after timeouts [FEATURE]
|
11
|
+
|
12
|
+
## 6.2.0
|
13
|
+
- Add validation for non-terminal conflict with wildcard [FEATURE]
|
14
|
+
|
15
|
+
## 6.1.2
|
16
|
+
- Retry on connection errors [FEATURE]
|
17
|
+
|
3
18
|
## 6.1.1
|
4
19
|
- Emit messages when waiting for rate-limit to elapse for DNSimple and NS1 providers, so deployment does not timeout [BUGFIX]
|
5
20
|
|
data/README.md
CHANGED
@@ -112,6 +112,10 @@ Changesets are how Record Store knows what updates to make. A `Changeset` is gen
|
|
112
112
|
|
113
113
|
When running `bin/record-store apply`, a `Changeset` is generated by comparing the current records in a zone's YAML file with the records the provider defines. A zone's YAML file is always considered the primary source of truth.
|
114
114
|
|
115
|
+
### Parallelism
|
116
|
+
|
117
|
+
Record store attempts to parallelize some of the bulk zone fetching operations. It does so by spawning multiple threads (default: 10). This value can be configured by setting the RECORD_STORE_MAX_THREADS environment variable to a positive integer value.
|
118
|
+
|
115
119
|
----
|
116
120
|
|
117
121
|
# Development
|
data/dev.yml
CHANGED
@@ -2,6 +2,9 @@ require 'resolv'
|
|
2
2
|
|
3
3
|
module RecordStore
|
4
4
|
class Provider
|
5
|
+
class Error < StandardError; end
|
6
|
+
class UnparseableBodyError < Error; end
|
7
|
+
|
5
8
|
class << self
|
6
9
|
def provider_for(object)
|
7
10
|
ns_server =
|
@@ -52,6 +55,10 @@ module RecordStore
|
|
52
55
|
false
|
53
56
|
end
|
54
57
|
|
58
|
+
def empty_non_terminal_over_wildcard?
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
55
62
|
def build_zone(zone_name:, config:)
|
56
63
|
zone = Zone.new(name: zone_name)
|
57
64
|
zone.records = retrieve_current_records(zone: zone_name)
|
@@ -128,6 +135,43 @@ module RecordStore
|
|
128
135
|
|
129
136
|
dns.getresource(zone_name, Resolv::DNS::Resource::IN::SOA).mname.to_s
|
130
137
|
end
|
138
|
+
|
139
|
+
def retry_on_connection_errors(
|
140
|
+
max_timeouts: 5,
|
141
|
+
max_conn_resets: 5,
|
142
|
+
max_retries: 5,
|
143
|
+
delay: 1,
|
144
|
+
backoff_multiplier: 2,
|
145
|
+
max_backoff: 10
|
146
|
+
)
|
147
|
+
waiter = BackoffWaiter.new(
|
148
|
+
'Waiting to retry after a connection reset',
|
149
|
+
initial_delay: delay,
|
150
|
+
multiplier: backoff_multiplier,
|
151
|
+
max_delay: max_backoff,
|
152
|
+
)
|
153
|
+
|
154
|
+
loop do
|
155
|
+
begin
|
156
|
+
return yield
|
157
|
+
rescue UnparseableBodyError
|
158
|
+
raise if max_retries <= 0
|
159
|
+
max_retries -= 1
|
160
|
+
|
161
|
+
waiter.wait(message: 'Waiting to retry after receiving an unparseable response')
|
162
|
+
rescue Net::OpenTimeout, Errno::ETIMEDOUT
|
163
|
+
raise if max_timeouts <= 0
|
164
|
+
max_timeouts -= 1
|
165
|
+
|
166
|
+
$stderr.puts('Retrying after a connection timeout')
|
167
|
+
rescue Errno::ECONNRESET
|
168
|
+
raise if max_conn_resets <= 0
|
169
|
+
max_conn_resets -= 1
|
170
|
+
|
171
|
+
waiter.wait
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
131
175
|
end
|
132
176
|
end
|
133
177
|
end
|
@@ -12,21 +12,29 @@ module RecordStore
|
|
12
12
|
true
|
13
13
|
end
|
14
14
|
|
15
|
+
def empty_non_terminal_over_wildcard?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
15
19
|
# returns an array of Record objects that match the records which exist in the provider
|
16
20
|
def retrieve_current_records(zone:, stdout: $stdout)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
retry_on_connection_errors do
|
22
|
+
session.zones.all_records(account_id, zone).data.map do |record|
|
23
|
+
begin
|
24
|
+
build_from_api(record, zone)
|
25
|
+
rescue StandardError
|
26
|
+
stdout.puts "Cannot build record: #{record}"
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end.compact
|
30
|
+
end
|
25
31
|
end
|
26
32
|
|
27
33
|
# Returns an array of the zones managed by provider as strings
|
28
34
|
def zones
|
29
|
-
|
35
|
+
retry_on_connection_errors do
|
36
|
+
session.zones.all_zones(account_id).data.map(&:name)
|
37
|
+
end
|
30
38
|
end
|
31
39
|
|
32
40
|
private
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '../provider_utils/
|
1
|
+
require_relative '../provider_utils/waiter'
|
2
2
|
|
3
3
|
# Patch Dnsimple client method which retrieves headers for API rate limit dynamically
|
4
4
|
module Dnsimple
|
@@ -26,8 +26,8 @@ module Dnsimple
|
|
26
26
|
rate_limit_periods = rate_limit_remaining + 1
|
27
27
|
sleep_time = rate_limit_reset_in / rate_limit_periods.to_f
|
28
28
|
|
29
|
-
rate_limit =
|
30
|
-
rate_limit.
|
29
|
+
rate_limit = RateLimitWaiter.new('DNSimple')
|
30
|
+
rate_limit.wait(sleep_time)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -3,8 +3,6 @@ require_relative 'ns1/patch_api_header'
|
|
3
3
|
|
4
4
|
module RecordStore
|
5
5
|
class Provider::NS1 < Provider
|
6
|
-
class Error < StandardError; end
|
7
|
-
|
8
6
|
class ApiAnswer
|
9
7
|
class << self
|
10
8
|
def from_full_api_answer(type:, record_id:, answer:)
|
@@ -60,14 +58,18 @@ module RecordStore
|
|
60
58
|
|
61
59
|
# Returns an array of the zones managed by provider as strings
|
62
60
|
def zones
|
63
|
-
|
61
|
+
retry_on_connection_errors do
|
62
|
+
client.zones.map { |zone| zone['zone'] }
|
63
|
+
end
|
64
64
|
end
|
65
65
|
|
66
66
|
private
|
67
67
|
|
68
68
|
# Fetches simplified records for the provided zone
|
69
69
|
def records_for_zone(zone)
|
70
|
-
|
70
|
+
retry_on_connection_errors do
|
71
|
+
client.zone(zone)['records']
|
72
|
+
end
|
71
73
|
end
|
72
74
|
|
73
75
|
# Creates a new record to the zone. It is expected this call modifies external state.
|
@@ -176,7 +178,7 @@ module RecordStore
|
|
176
178
|
unless updated
|
177
179
|
error = +'while trying to update a record, could not find answer with fqdn: '
|
178
180
|
error << "#{record.fqdn}, type; #{record.type}, id: #{id}"
|
179
|
-
raise Error, error
|
181
|
+
raise RecordStore::Provider::Error, error
|
180
182
|
end
|
181
183
|
|
182
184
|
client.modify_record(
|
@@ -1,46 +1,59 @@
|
|
1
|
+
require 'net/http'
|
1
2
|
require 'ns1'
|
2
3
|
|
3
4
|
module RecordStore
|
4
5
|
class Provider::NS1 < Provider
|
5
|
-
class Error < StandardError; end
|
6
|
-
|
7
6
|
class Client < ::NS1::Client
|
8
7
|
def initialize(api_key:)
|
9
8
|
super(api_key)
|
10
9
|
end
|
11
10
|
|
12
11
|
def zones
|
13
|
-
super
|
12
|
+
zones = super
|
13
|
+
raise_if_error!(zones)
|
14
|
+
zones
|
14
15
|
end
|
15
16
|
|
16
17
|
def zone(name)
|
17
|
-
super(name)
|
18
|
+
zone = super(name)
|
19
|
+
raise_if_error!(zone)
|
20
|
+
zone
|
18
21
|
end
|
19
22
|
|
20
23
|
def record(zone:, fqdn:, type:, must_exist: false)
|
21
24
|
result = super(zone, fqdn, type)
|
22
|
-
|
25
|
+
raise_if_error!(result) if must_exist
|
23
26
|
return nil if result.is_a?(NS1::Response::Error)
|
24
27
|
result
|
25
28
|
end
|
26
29
|
|
27
30
|
def create_record(zone:, fqdn:, type:, params:)
|
28
31
|
result = super(zone, fqdn, type, params)
|
29
|
-
|
32
|
+
raise_if_error!(result)
|
30
33
|
nil
|
31
34
|
end
|
32
35
|
|
33
36
|
def modify_record(zone:, fqdn:, type:, params:)
|
34
37
|
result = super(zone, fqdn, type, params)
|
35
|
-
|
38
|
+
raise_if_error!(result)
|
36
39
|
nil
|
37
40
|
end
|
38
41
|
|
39
42
|
def delete_record(zone:, fqdn:, type:)
|
40
43
|
result = super(zone, fqdn, type)
|
41
|
-
|
44
|
+
raise_if_error!(result)
|
42
45
|
nil
|
43
46
|
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def raise_if_error!(result)
|
51
|
+
return unless result.is_a?(NS1::Response::Error)
|
52
|
+
if result.is_a?(NS1::Response::UnparsableBodyError)
|
53
|
+
raise RecordStore::Provider::UnparseableBodyError, result.to_s
|
54
|
+
end
|
55
|
+
raise RecordStore::Provider::Error, result.to_s
|
56
|
+
end
|
44
57
|
end
|
45
58
|
end
|
46
59
|
end
|
@@ -1,25 +1,41 @@
|
|
1
1
|
require 'net/http'
|
2
|
-
require_relative '../provider_utils/
|
2
|
+
require_relative '../provider_utils/waiter'
|
3
|
+
|
4
|
+
class NS1::Response::UnparsableBodyError < NS1::Response::Error
|
5
|
+
def initialize(status)
|
6
|
+
@status = status
|
7
|
+
super({}, status)
|
8
|
+
end
|
9
|
+
end
|
3
10
|
|
4
11
|
# Patch the method which retrieves headers for API rate limit dynamically
|
5
12
|
module NS1::Transport
|
6
13
|
class NetHttp
|
14
|
+
X_RATELIMIT_PERIOD = 'x-ratelimit-period'.freeze
|
15
|
+
X_RATELIMIT_REMAINING = 'x-ratelimit-remaining'.freeze
|
16
|
+
|
7
17
|
def process_response(response)
|
8
|
-
|
9
|
-
|
18
|
+
response_hash = response.to_hash
|
19
|
+
|
20
|
+
if response_hash.key?(X_RATELIMIT_PERIOD) && response_hash.key?(X_RATELIMIT_REMAINING)
|
21
|
+
sleep_time = response_hash[X_RATELIMIT_PERIOD].first.to_i /
|
22
|
+
[1, response_hash[X_RATELIMIT_REMAINING].first.to_i].max.to_f
|
10
23
|
|
11
|
-
|
12
|
-
|
24
|
+
rate_limit = RateLimitWaiter.new('NS1')
|
25
|
+
rate_limit.wait(sleep_time)
|
26
|
+
end
|
13
27
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
28
|
+
begin
|
29
|
+
body = JSON.parse(response.body)
|
30
|
+
case response
|
31
|
+
when Net::HTTPOK
|
32
|
+
NS1::Response::Success.new(body, response.code.to_i)
|
33
|
+
else
|
34
|
+
NS1::Response::Error.new(body, response.code.to_i)
|
35
|
+
end
|
36
|
+
rescue JSON::ParserError
|
37
|
+
NS1::Response::UnparsableBodyError.new(response.code.to_i)
|
20
38
|
end
|
21
|
-
rescue JSON::ParserError
|
22
|
-
raise NS1::Transport::ResponseParseError
|
23
39
|
end
|
24
40
|
end
|
25
41
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Waiter
|
2
|
+
def initialize(message = nil)
|
3
|
+
@message = message || 'Waiting'
|
4
|
+
end
|
5
|
+
|
6
|
+
attr_accessor :message
|
7
|
+
|
8
|
+
def wait(sleep_time, message: @message)
|
9
|
+
while sleep_time > 0
|
10
|
+
wait_time = [10, sleep_time].min
|
11
|
+
puts "#{message} (#{sleep_time}s left)" if wait_time > 1
|
12
|
+
sleep(wait_time)
|
13
|
+
sleep_time -= wait_time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class RateLimitWaiter < Waiter
|
19
|
+
def initialize(provider)
|
20
|
+
super("Waiting on #{provider} rate-limit")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BackoffWaiter < Waiter
|
25
|
+
def initialize(message, initial_delay:, multiplier:, max_delay: nil)
|
26
|
+
super(message)
|
27
|
+
|
28
|
+
@initial_delay = @current_delay = initial_delay
|
29
|
+
@multiplier = multiplier
|
30
|
+
@max_delay = max_delay
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset
|
34
|
+
@current_delay = @initial_delay
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait(message: @message)
|
38
|
+
super(@current_delay, message: message)
|
39
|
+
@current_delay = [@current_delay * @multiplier, @max_delay].compact.min
|
40
|
+
end
|
41
|
+
end
|
data/lib/record_store/record.rb
CHANGED
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -18,6 +18,7 @@ module RecordStore
|
|
18
18
|
validate :validate_cname_records_dont_point_to_root
|
19
19
|
validate :validate_same_ttl_for_records_sharing_fqdn_and_type
|
20
20
|
validate :validate_provider_can_handle_zone_records
|
21
|
+
validate :validate_no_empty_non_terminal
|
21
22
|
validate :validate_can_handle_alias_records
|
22
23
|
|
23
24
|
class << self
|
@@ -44,13 +45,18 @@ module RecordStore
|
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
47
|
-
|
48
|
+
DEFAULT_MAX_PARALLEL_THREADS = 10
|
49
|
+
|
50
|
+
def max_parallel_threads
|
51
|
+
(ENV['RECORD_STORE_MAX_THREADS'] || DEFAULT_MAX_PARALLEL_THREADS).to_i
|
52
|
+
end
|
53
|
+
|
48
54
|
def modified(verbose: false) # rubocop:disable Lint/UnusedMethodArgument
|
49
55
|
modified_zones = []
|
50
56
|
mutex = Mutex.new
|
51
57
|
zones = all
|
52
58
|
|
53
|
-
(1..
|
59
|
+
(1..max_parallel_threads).map do
|
54
60
|
Thread.new do
|
55
61
|
current_zone = nil
|
56
62
|
while zones.any?
|
@@ -258,6 +264,28 @@ module RecordStore
|
|
258
264
|
end
|
259
265
|
end
|
260
266
|
|
267
|
+
def validate_no_empty_non_terminal
|
268
|
+
return unless config.empty_non_terminal_over_wildcard?
|
269
|
+
|
270
|
+
wildcards = records.select(&:wildcard?).map(&:fqdn).uniq
|
271
|
+
wildcards.each do |wildcard|
|
272
|
+
suffix = wildcard[1..-1]
|
273
|
+
|
274
|
+
terminal_records = records.map(&:fqdn)
|
275
|
+
.select { |record| record.match?(/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_])#{Regexp.escape(suffix)}$/) }
|
276
|
+
next unless terminal_records.any?
|
277
|
+
|
278
|
+
intermediate_records = records.map(&:fqdn)
|
279
|
+
.select { |record| record.match?(/^([a-zA-Z0-9\-_]+)#{Regexp.escape(suffix)}$/) }
|
280
|
+
terminal_records.each do |terminal_record|
|
281
|
+
non_terminal = terminal_record.partition('.').last
|
282
|
+
errors.add(:records, "found empty non-terminal #{non_terminal} "\
|
283
|
+
"(caused by existing records #{wildcard} and #{terminal_record})")\
|
284
|
+
unless intermediate_records.include?(non_terminal)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
261
289
|
def validate_can_handle_alias_records
|
262
290
|
return unless records.any? { |record| record.is_a?(Record::ALIAS) }
|
263
291
|
return if config.supports_alias?
|
@@ -24,6 +24,10 @@ module RecordStore
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
def empty_non_terminal_over_wildcard?
|
28
|
+
valid_providers? && providers.any? { |provider| Provider.const_get(provider).empty_non_terminal_over_wildcard? }
|
29
|
+
end
|
30
|
+
|
27
31
|
def to_hash
|
28
32
|
config_hash = {
|
29
33
|
providers: providers,
|
data/record_store.gemspec
CHANGED
@@ -48,6 +48,6 @@ Gem::Specification.new do |spec|
|
|
48
48
|
spec.add_development_dependency 'vcr'
|
49
49
|
spec.add_development_dependency 'pry'
|
50
50
|
spec.add_development_dependency 'webmock'
|
51
|
-
spec.add_development_dependency 'rubocop'
|
51
|
+
spec.add_development_dependency 'rubocop', '0.89.1'
|
52
52
|
spec.add_development_dependency 'minitest-focus'
|
53
53
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: record_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-10-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -301,16 +301,16 @@ dependencies:
|
|
301
301
|
name: rubocop
|
302
302
|
requirement: !ruby/object:Gem::Requirement
|
303
303
|
requirements:
|
304
|
-
- -
|
304
|
+
- - '='
|
305
305
|
- !ruby/object:Gem::Version
|
306
|
-
version:
|
306
|
+
version: 0.89.1
|
307
307
|
type: :development
|
308
308
|
prerelease: false
|
309
309
|
version_requirements: !ruby/object:Gem::Requirement
|
310
310
|
requirements:
|
311
|
-
- -
|
311
|
+
- - '='
|
312
312
|
- !ruby/object:Gem::Version
|
313
|
-
version:
|
313
|
+
version: 0.89.1
|
314
314
|
- !ruby/object:Gem::Dependency
|
315
315
|
name: minitest-focus
|
316
316
|
requirement: !ruby/object:Gem::Requirement
|
@@ -362,7 +362,7 @@ files:
|
|
362
362
|
- lib/record_store/provider/ns1/client.rb
|
363
363
|
- lib/record_store/provider/ns1/patch_api_header.rb
|
364
364
|
- lib/record_store/provider/oracle_cloud_dns.rb
|
365
|
-
- lib/record_store/provider/provider_utils/
|
365
|
+
- lib/record_store/provider/provider_utils/waiter.rb
|
366
366
|
- lib/record_store/record.rb
|
367
367
|
- lib/record_store/record/a.rb
|
368
368
|
- lib/record_store/record/aaaa.rb
|
@@ -1,14 +0,0 @@
|
|
1
|
-
class RateLimit
|
2
|
-
def initialize(provider)
|
3
|
-
@provider = provider
|
4
|
-
end
|
5
|
-
|
6
|
-
def sleep_for(sleep_time)
|
7
|
-
while sleep_time > 0
|
8
|
-
wait = [10, sleep_time].min
|
9
|
-
puts "Waiting on #{@provider} rate-limit" if wait > 1
|
10
|
-
sleep(wait)
|
11
|
-
sleep_time -= wait
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|