record_store 6.1.1 → 6.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|