hearth 1.0.0.pre1 → 1.0.0.pre2
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/CHANGELOG.md +4 -4
- data/VERSION +1 -1
- data/lib/hearth/api_error.rb +15 -1
- data/lib/hearth/auth_option.rb +21 -0
- data/lib/hearth/auth_schemes/anonymous.rb +21 -0
- data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
- data/lib/hearth/auth_schemes/http_basic.rb +16 -0
- data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
- data/lib/hearth/auth_schemes/http_digest.rb +16 -0
- data/lib/hearth/auth_schemes.rb +32 -0
- data/lib/hearth/checksums.rb +31 -0
- data/lib/hearth/client_stubs.rb +130 -0
- data/lib/hearth/config/env_provider.rb +53 -0
- data/lib/hearth/config/resolver.rb +52 -0
- data/lib/hearth/configuration.rb +15 -0
- data/lib/hearth/connection_pool.rb +77 -0
- data/lib/hearth/context.rb +28 -4
- data/lib/hearth/dns/host_address.rb +23 -0
- data/lib/hearth/dns/host_resolver.rb +92 -0
- data/lib/hearth/dns.rb +48 -0
- data/lib/hearth/http/api_error.rb +4 -8
- data/lib/hearth/http/client.rb +208 -59
- data/lib/hearth/http/error_inspector.rb +85 -0
- data/lib/hearth/http/error_parser.rb +18 -20
- data/lib/hearth/http/field.rb +64 -0
- data/lib/hearth/http/fields.rb +117 -0
- data/lib/hearth/http/middleware/content_length.rb +5 -2
- data/lib/hearth/http/middleware/content_md5.rb +31 -0
- data/lib/hearth/http/middleware/request_compression.rb +157 -0
- data/lib/hearth/http/middleware.rb +12 -0
- data/lib/hearth/http/networking_error.rb +1 -14
- data/lib/hearth/http/request.rb +83 -56
- data/lib/hearth/http/response.rb +42 -13
- data/lib/hearth/http.rb +14 -5
- data/lib/hearth/identities/anonymous.rb +8 -0
- data/lib/hearth/identities/http_api_key.rb +16 -0
- data/lib/hearth/identities/http_bearer.rb +16 -0
- data/lib/hearth/identities/http_login.rb +20 -0
- data/lib/hearth/identities.rb +21 -0
- data/lib/hearth/identity_resolver.rb +17 -0
- data/lib/hearth/interceptor.rb +506 -0
- data/lib/hearth/interceptor_context.rb +36 -0
- data/lib/hearth/interceptor_list.rb +48 -0
- data/lib/hearth/interceptors.rb +75 -0
- data/lib/hearth/middleware/auth.rb +100 -0
- data/lib/hearth/middleware/build.rb +32 -0
- data/lib/hearth/middleware/host_prefix.rb +10 -6
- data/lib/hearth/middleware/initialize.rb +58 -0
- data/lib/hearth/middleware/parse.rb +45 -6
- data/lib/hearth/middleware/retry.rb +97 -23
- data/lib/hearth/middleware/send.rb +137 -25
- data/lib/hearth/middleware/sign.rb +65 -0
- data/lib/hearth/middleware/validate.rb +11 -1
- data/lib/hearth/middleware.rb +19 -8
- data/lib/hearth/middleware_stack.rb +1 -43
- data/lib/hearth/networking_error.rb +18 -0
- data/lib/hearth/number_helper.rb +2 -2
- data/lib/hearth/output.rb +8 -4
- data/lib/hearth/plugin_list.rb +53 -0
- data/lib/hearth/query/param.rb +52 -0
- data/lib/hearth/query/param_list.rb +54 -0
- data/lib/hearth/query/param_matcher.rb +32 -0
- data/lib/hearth/refreshing_identity_resolver.rb +63 -0
- data/lib/hearth/request.rb +22 -0
- data/lib/hearth/response.rb +33 -0
- data/lib/hearth/retry/adaptive.rb +60 -0
- data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
- data/lib/hearth/retry/client_rate_limiter.rb +143 -0
- data/lib/hearth/retry/exponential_backoff.rb +15 -0
- data/lib/hearth/retry/retry_quota.rb +56 -0
- data/lib/hearth/retry/standard.rb +46 -0
- data/lib/hearth/retry/strategy.rb +20 -0
- data/lib/hearth/retry.rb +16 -0
- data/lib/hearth/signers/anonymous.rb +16 -0
- data/lib/hearth/signers/http_api_key.rb +29 -0
- data/lib/hearth/signers/http_basic.rb +23 -0
- data/lib/hearth/signers/http_bearer.rb +19 -0
- data/lib/hearth/signers/http_digest.rb +19 -0
- data/lib/hearth/signers.rb +23 -0
- data/lib/hearth/stubs.rb +30 -0
- data/lib/hearth/time_helper.rb +5 -3
- data/lib/hearth/validator.rb +44 -5
- data/lib/hearth/waiters/poller.rb +6 -7
- data/lib/hearth/waiters/waiter.rb +17 -4
- data/lib/hearth/xml/formatter.rb +11 -2
- data/lib/hearth/xml/node.rb +2 -2
- data/lib/hearth.rb +32 -5
- data/sig/lib/hearth/aliases.rbs +4 -0
- data/sig/lib/hearth/api_error.rbs +13 -0
- data/sig/lib/hearth/auth_option.rbs +11 -0
- data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
- data/sig/lib/hearth/auth_schemes.rbs +13 -0
- data/sig/lib/hearth/block_io.rbs +9 -0
- data/sig/lib/hearth/client_stubs.rbs +5 -0
- data/sig/lib/hearth/configuration.rbs +7 -0
- data/sig/lib/hearth/dns/host_address.rbs +13 -0
- data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
- data/sig/lib/hearth/http/api_error.rbs +13 -0
- data/sig/lib/hearth/http/client.rbs +9 -0
- data/sig/lib/hearth/http/field.rbs +19 -0
- data/sig/lib/hearth/http/fields.rbs +43 -0
- data/sig/lib/hearth/http/request.rbs +25 -0
- data/sig/lib/hearth/http/response.rbs +21 -0
- data/sig/lib/hearth/identities/anonymous.rbs +6 -0
- data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
- data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
- data/sig/lib/hearth/identities/http_login.rbs +11 -0
- data/sig/lib/hearth/identities.rbs +9 -0
- data/sig/lib/hearth/identity_resolver.rbs +7 -0
- data/sig/lib/hearth/interceptor.rbs +9 -0
- data/sig/lib/hearth/interceptor_context.rbs +15 -0
- data/sig/lib/hearth/interceptor_list.rbs +16 -0
- data/sig/lib/hearth/interfaces.rbs +65 -0
- data/sig/lib/hearth/output.rbs +11 -0
- data/sig/lib/hearth/plugin_list.rbs +15 -0
- data/sig/lib/hearth/query/param.rbs +17 -0
- data/sig/lib/hearth/query/param_list.rbs +25 -0
- data/sig/lib/hearth/request.rbs +9 -0
- data/sig/lib/hearth/response.rbs +11 -0
- data/sig/lib/hearth/retry/adaptive.rbs +13 -0
- data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
- data/sig/lib/hearth/retry/standard.rbs +13 -0
- data/sig/lib/hearth/retry/strategy.rbs +11 -0
- data/sig/lib/hearth/retry.rbs +9 -0
- data/sig/lib/hearth/signers/anonymous.rbs +9 -0
- data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
- data/sig/lib/hearth/signers/http_basic.rbs +9 -0
- data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
- data/sig/lib/hearth/signers/http_digest.rbs +9 -0
- data/sig/lib/hearth/signers.rbs +9 -0
- data/sig/lib/hearth/structure.rbs +7 -0
- data/sig/lib/hearth/union.rbs +5 -0
- data/sig/lib/hearth/waiters/waiter.rbs +17 -0
- metadata +132 -22
- data/lib/hearth/http/headers.rb +0 -70
- data/lib/hearth/middleware/around_handler.rb +0 -24
- data/lib/hearth/middleware/request_handler.rb +0 -24
- data/lib/hearth/middleware/response_handler.rb +0 -25
- data/lib/hearth/middleware_builder.rb +0 -246
- data/lib/hearth/stubbing/client_stubs.rb +0 -115
- data/lib/hearth/stubbing/stubs.rb +0 -32
- data/lib/hearth/waiters/errors.rb +0 -15
- data/sig/lib/seahorse/api_error.rbs +0 -10
- data/sig/lib/seahorse/document.rbs +0 -2
- data/sig/lib/seahorse/http/api_error.rbs +0 -21
- data/sig/lib/seahorse/http/headers.rbs +0 -47
- data/sig/lib/seahorse/http/response.rbs +0 -21
- data/sig/lib/seahorse/simple_delegator.rbs +0 -3
- data/sig/lib/seahorse/structure.rbs +0 -18
- data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
- data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
- data/sig/lib/seahorse/union.rbs +0 -6
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Retry
|
|
5
|
+
# @api private
|
|
6
|
+
# Used only in 'adaptive' retry mode
|
|
7
|
+
class ClientRateLimiter
|
|
8
|
+
MIN_CAPACITY = 1
|
|
9
|
+
MIN_FILL_RATE = 0.5
|
|
10
|
+
SMOOTH = 0.8
|
|
11
|
+
# How much to scale back after a throttling response
|
|
12
|
+
BETA = 0.7
|
|
13
|
+
# Controls how aggressively we scale up after being throttled
|
|
14
|
+
SCALE_CONSTANT = 0.4
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@fill_rate = nil
|
|
19
|
+
@max_capacity = nil
|
|
20
|
+
@current_capacity = 0
|
|
21
|
+
@last_timestamp = nil
|
|
22
|
+
@enabled = false
|
|
23
|
+
@measured_tx_rate = 0
|
|
24
|
+
@last_tx_rate_bucket = monotonic_seconds
|
|
25
|
+
@request_count = 0
|
|
26
|
+
@last_max_rate = 0
|
|
27
|
+
@last_throttle_time = monotonic_seconds
|
|
28
|
+
@calculated_rate = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def token_bucket_acquire(amount, wait_to_fill: true)
|
|
32
|
+
# Client side throttling is not enabled until we see a
|
|
33
|
+
# throttling error
|
|
34
|
+
return unless @enabled
|
|
35
|
+
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
token_bucket_refill
|
|
38
|
+
|
|
39
|
+
# Next see if we have enough capacity for the requested amount
|
|
40
|
+
while @current_capacity < amount
|
|
41
|
+
raise CapacityNotAvailableError unless wait_to_fill
|
|
42
|
+
|
|
43
|
+
@mutex.sleep((amount - @current_capacity) / @fill_rate)
|
|
44
|
+
token_bucket_refill
|
|
45
|
+
end
|
|
46
|
+
@current_capacity -= amount
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def update_sending_rate(is_throttling_error)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
update_measured_rate
|
|
53
|
+
|
|
54
|
+
if is_throttling_error
|
|
55
|
+
rate_to_use = if @enabled
|
|
56
|
+
[@measured_tx_rate, @fill_rate].min
|
|
57
|
+
else
|
|
58
|
+
@measured_tx_rate
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The fill_rate is from the token bucket
|
|
62
|
+
@last_max_rate = rate_to_use
|
|
63
|
+
calculate_time_window
|
|
64
|
+
@last_throttle_time = monotonic_seconds
|
|
65
|
+
@calculated_rate = cubic_throttle(rate_to_use)
|
|
66
|
+
enable_token_bucket
|
|
67
|
+
else
|
|
68
|
+
calculate_time_window
|
|
69
|
+
@calculated_rate = cubic_success(monotonic_seconds)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
new_rate = [@calculated_rate, 2 * @measured_tx_rate].min
|
|
73
|
+
token_bucket_update_rate(new_rate)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def monotonic_seconds
|
|
80
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def token_bucket_refill
|
|
84
|
+
timestamp = monotonic_seconds
|
|
85
|
+
unless @last_timestamp
|
|
86
|
+
@last_timestamp = timestamp
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
fill_amount = (timestamp - @last_timestamp) * @fill_rate
|
|
91
|
+
@current_capacity = [
|
|
92
|
+
@max_capacity, @current_capacity + fill_amount
|
|
93
|
+
].min
|
|
94
|
+
|
|
95
|
+
@last_timestamp = timestamp
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def token_bucket_update_rate(new_rps)
|
|
99
|
+
# Refill based on our current rate before we update to the
|
|
100
|
+
# new fill rate
|
|
101
|
+
token_bucket_refill
|
|
102
|
+
@fill_rate = [new_rps, MIN_FILL_RATE].max
|
|
103
|
+
@max_capacity = [new_rps, MIN_CAPACITY].max
|
|
104
|
+
# When we scale down we can't have a current capacity that exceeds our
|
|
105
|
+
# max_capacity.
|
|
106
|
+
@current_capacity = [@current_capacity, @max_capacity].min
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def enable_token_bucket
|
|
110
|
+
@enabled = true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def update_measured_rate
|
|
114
|
+
t = monotonic_seconds
|
|
115
|
+
time_bucket = (t * 2).floor / 2.0
|
|
116
|
+
@request_count += 1
|
|
117
|
+
return unless time_bucket > @last_tx_rate_bucket
|
|
118
|
+
|
|
119
|
+
current_rate = @request_count / (time_bucket - @last_tx_rate_bucket)
|
|
120
|
+
@measured_tx_rate = (current_rate * SMOOTH) +
|
|
121
|
+
(@measured_tx_rate * (1 - SMOOTH))
|
|
122
|
+
@request_count = 0
|
|
123
|
+
@last_tx_rate_bucket = time_bucket
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def calculate_time_window
|
|
127
|
+
# This is broken out into a separate calculation because it only
|
|
128
|
+
# gets updated when @last_max_rate changes so it can be cached.
|
|
129
|
+
base = ((@last_max_rate * (1 - BETA)) / SCALE_CONSTANT)
|
|
130
|
+
@time_window = base**(1.0 / 3)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cubic_success(timestamp)
|
|
134
|
+
dt = timestamp - @last_throttle_time
|
|
135
|
+
(SCALE_CONSTANT * ((dt - @time_window)**3)) + @last_max_rate
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def cubic_throttle(rate_to_use)
|
|
139
|
+
rate_to_use * BETA
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Retry
|
|
5
|
+
# Computes an exponential backoff delay for a retry attempt.
|
|
6
|
+
class ExponentialBackoff
|
|
7
|
+
# Max backoff (in seconds)
|
|
8
|
+
MAX_BACKOFF = 20
|
|
9
|
+
|
|
10
|
+
def call(attempts)
|
|
11
|
+
[Kernel.rand * (2**attempts), MAX_BACKOFF].min
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Retry
|
|
5
|
+
# @api private
|
|
6
|
+
# Used in 'standard' and 'adaptive' retry modes.
|
|
7
|
+
class RetryQuota
|
|
8
|
+
INITIAL_RETRY_TOKENS = 500
|
|
9
|
+
RETRY_COST = 5
|
|
10
|
+
NO_RETRY_INCREMENT = 1
|
|
11
|
+
TIMEOUT_RETRY_COST = 10
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@max_capacity = INITIAL_RETRY_TOKENS
|
|
16
|
+
@available_capacity = @max_capacity
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if there is sufficient capacity to retry and return it.
|
|
20
|
+
# If there is insufficient capacity, return 0
|
|
21
|
+
# @return [Integer] The amount of capacity checked out
|
|
22
|
+
def checkout_capacity(error_info)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
capacity_amount =
|
|
25
|
+
if error_info.error_type == 'Transient'
|
|
26
|
+
TIMEOUT_RETRY_COST
|
|
27
|
+
else
|
|
28
|
+
RETRY_COST
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# unable to acquire capacity
|
|
32
|
+
return 0 if capacity_amount > @available_capacity
|
|
33
|
+
|
|
34
|
+
@available_capacity -= capacity_amount
|
|
35
|
+
capacity_amount
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# capacity_amount refers to the amount of capacity requested from
|
|
40
|
+
# the last retry. It can either be RETRY_COST, TIMEOUT_RETRY_COST,
|
|
41
|
+
# or unset.
|
|
42
|
+
def release(capacity_amount)
|
|
43
|
+
# Implementation note: The release() method is called for
|
|
44
|
+
# every API call. In the common case where the request is
|
|
45
|
+
# successful and we're at full capacity, we can avoid locking.
|
|
46
|
+
# We can't exceed max capacity so there's no work we have to do.
|
|
47
|
+
return if @available_capacity == @max_capacity
|
|
48
|
+
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
@available_capacity += capacity_amount || NO_RETRY_INCREMENT
|
|
51
|
+
@available_capacity = [@available_capacity, @max_capacity].min
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Retry
|
|
5
|
+
# Standard retry strategy for retrying requests.
|
|
6
|
+
class Standard < Strategy
|
|
7
|
+
# @param [#call] backoff (ExponentialBackoff) A callable object that
|
|
8
|
+
# calculates a backoff delay for a retry attempt.
|
|
9
|
+
# @param [Integer] max_attempts (3) The maximum number of attempts that
|
|
10
|
+
# will be made for a single request, including the initial attempt.
|
|
11
|
+
def initialize(backoff: ExponentialBackoff.new, max_attempts: 3)
|
|
12
|
+
super()
|
|
13
|
+
@backoff = backoff
|
|
14
|
+
@max_attempts = max_attempts
|
|
15
|
+
|
|
16
|
+
# instance state
|
|
17
|
+
@retry_quota = RetryQuota.new
|
|
18
|
+
@capacity_amount = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def acquire_initial_retry_token(_token_scope = nil)
|
|
22
|
+
Token.new(retry_count: 0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def refresh_retry_token(retry_token, error_info)
|
|
26
|
+
return unless error_info.retryable?
|
|
27
|
+
|
|
28
|
+
return if retry_token.retry_count >= @max_attempts - 1
|
|
29
|
+
|
|
30
|
+
@capacity_amount = @retry_quota.checkout_capacity(error_info)
|
|
31
|
+
return unless @capacity_amount.positive?
|
|
32
|
+
|
|
33
|
+
delay = error_info.hints[:retry_after]
|
|
34
|
+
delay ||= @backoff.call(retry_token.retry_count)
|
|
35
|
+
retry_token.retry_count += 1
|
|
36
|
+
retry_token.retry_delay = delay
|
|
37
|
+
retry_token
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def record_success(retry_token)
|
|
41
|
+
@retry_quota.release(@capacity_amount)
|
|
42
|
+
retry_token
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Retry
|
|
5
|
+
# Interface for retry strategies.
|
|
6
|
+
class Strategy
|
|
7
|
+
def acquire_initial_retry_token(_token_scope = nil)
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def refresh_retry_token(_retry_token, _error_info)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def record_success(_retry_token)
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/hearth/retry.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'retry/strategy'
|
|
4
|
+
|
|
5
|
+
require_relative 'retry/adaptive'
|
|
6
|
+
require_relative 'retry/capacity_not_available_error'
|
|
7
|
+
require_relative 'retry/client_rate_limiter'
|
|
8
|
+
require_relative 'retry/exponential_backoff'
|
|
9
|
+
require_relative 'retry/retry_quota'
|
|
10
|
+
require_relative 'retry/standard'
|
|
11
|
+
|
|
12
|
+
module Hearth
|
|
13
|
+
module Retry
|
|
14
|
+
Token = Struct.new(:retry_count, :retry_delay, keyword_init: true)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Signers
|
|
5
|
+
# A signer that does not sign requests.
|
|
6
|
+
class Anonymous < Signers::Base
|
|
7
|
+
def sign(request:, identity:, properties:)
|
|
8
|
+
# Do nothing.
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def reset(request:, properties:)
|
|
12
|
+
# Do nothing.
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Signers
|
|
5
|
+
# A signer that signs requests using the HTTP API Key Auth scheme.
|
|
6
|
+
class HTTPApiKey < Signers::Base
|
|
7
|
+
def sign(request:, identity:, properties:)
|
|
8
|
+
case properties[:in]
|
|
9
|
+
when 'header'
|
|
10
|
+
value = "#{properties[:scheme]} #{identity.key}".strip
|
|
11
|
+
request.headers[properties[:name]] = value
|
|
12
|
+
when 'query'
|
|
13
|
+
name = properties[:name]
|
|
14
|
+
request.append_query_param(name, identity.key)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset(request:, properties:)
|
|
19
|
+
case properties[:in]
|
|
20
|
+
when 'header'
|
|
21
|
+
request.headers.delete(properties[:name])
|
|
22
|
+
when 'query'
|
|
23
|
+
name = properties[:name]
|
|
24
|
+
request.remove_query_param(name)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Hearth
|
|
6
|
+
module Signers
|
|
7
|
+
# A signer that signs requests using the HTTP Basic Auth scheme.
|
|
8
|
+
class HTTPBasic
|
|
9
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
10
|
+
def sign(request:, identity:, properties:)
|
|
11
|
+
# TODO: does not handle realm or other properties
|
|
12
|
+
identity_string = "#{identity.username}:#{identity.password}"
|
|
13
|
+
encoded = Base64.strict_encode64(identity_string)
|
|
14
|
+
request.headers['Authorization'] = "Basic #{encoded}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset(request:, properties:)
|
|
18
|
+
request.headers.delete('Authorization')
|
|
19
|
+
end
|
|
20
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Signers
|
|
5
|
+
# A signer that signs requests using the HTTP Bearer Auth scheme.
|
|
6
|
+
class HTTPBearer < Signers::Base
|
|
7
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
8
|
+
def sign(request:, identity:, properties:)
|
|
9
|
+
# TODO: does not handle realm or other properties
|
|
10
|
+
request.headers['Authorization'] = "Bearer #{identity.token}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset(request:, properties:)
|
|
14
|
+
request.headers.delete('Authorization')
|
|
15
|
+
end
|
|
16
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Signers
|
|
5
|
+
# A signer that signs requests using the HTTP Digest Auth scheme.
|
|
6
|
+
class HTTPDigest < Signers::Base
|
|
7
|
+
def sign(request:, identity:, properties:)
|
|
8
|
+
# TODO: requires a nonce from the server - this cannot
|
|
9
|
+
# be implemented unless we rescue from a 401 and retry
|
|
10
|
+
# with the nonce
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset(request:, properties:)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
# Namespace for all Signer classes.
|
|
5
|
+
module Signers
|
|
6
|
+
# Base class for all Signer classes.
|
|
7
|
+
class Base
|
|
8
|
+
def sign(request:, identity:, properties:)
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def reset(request:, properties:)
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative 'signers/anonymous'
|
|
20
|
+
require_relative 'signers/http_api_key'
|
|
21
|
+
require_relative 'signers/http_basic'
|
|
22
|
+
require_relative 'signers/http_bearer'
|
|
23
|
+
require_relative 'signers/http_digest'
|
data/lib/hearth/stubs.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
# Provides a thread safe data structure for adding and getting stubs
|
|
5
|
+
# per operation.
|
|
6
|
+
# @api private
|
|
7
|
+
class Stubs
|
|
8
|
+
def initialize
|
|
9
|
+
@stubs = {}
|
|
10
|
+
@stub_mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add_stubs(operation_name, stubs)
|
|
14
|
+
@stub_mutex.synchronize do
|
|
15
|
+
@stubs[operation_name.to_sym] = stubs
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next(operation_name)
|
|
20
|
+
@stub_mutex.synchronize do
|
|
21
|
+
stubs = @stubs[operation_name] || []
|
|
22
|
+
case stubs.length
|
|
23
|
+
when 0 then nil
|
|
24
|
+
when 1 then stubs.first
|
|
25
|
+
else stubs.shift
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/hearth/time_helper.rb
CHANGED
|
@@ -11,11 +11,12 @@ module Hearth
|
|
|
11
11
|
# @param [Time] time
|
|
12
12
|
# @return [String<Date Time>] The time as an ISO8601 string.
|
|
13
13
|
def to_date_time(time)
|
|
14
|
-
time.
|
|
14
|
+
optional_ms_digits = time.subsec.zero? ? nil : 3
|
|
15
|
+
time.utc.iso8601(optional_ms_digits)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# @param [Time] time
|
|
18
|
-
# @return [Float<Epoch Seconds>] Returns float value of
|
|
19
|
+
# @return [Float<Epoch Seconds>] Returns the float value of
|
|
19
20
|
# epoch seconds with millisecond precision.
|
|
20
21
|
def to_epoch_seconds(time)
|
|
21
22
|
time = time.utc
|
|
@@ -28,7 +29,8 @@ module Hearth
|
|
|
28
29
|
# @return [String<Http Date>] Returns the time formatted
|
|
29
30
|
# as an HTTP header date.
|
|
30
31
|
def to_http_date(time)
|
|
31
|
-
time.
|
|
32
|
+
fractional = '.%L' unless time.subsec.zero?
|
|
33
|
+
time.utc.strftime("%a, %d %b %Y %H:%M:%S#{fractional} GMT")
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
data/lib/hearth/validator.rb
CHANGED
|
@@ -3,18 +3,57 @@
|
|
|
3
3
|
module Hearth
|
|
4
4
|
# Utility module for working with request parameters.
|
|
5
5
|
#
|
|
6
|
-
# * Validate structure of parameters against the expected
|
|
6
|
+
# * Validate structure of parameters against the expected input.
|
|
7
7
|
# * Raise errors with context when validation fails.
|
|
8
8
|
# @api private
|
|
9
9
|
module Validator
|
|
10
|
-
# Validate the given
|
|
10
|
+
# Validate the given value is of the given type(s).
|
|
11
|
+
# @param value [Object] The value to validate.
|
|
12
|
+
# @param types [Array<Class>] The types to validate against.
|
|
13
|
+
# @param context [String] The context of the value being validated.
|
|
11
14
|
# @raise [ArgumentError] Raises when the value is not one of given type(s).
|
|
12
|
-
def self.
|
|
13
|
-
return if
|
|
15
|
+
def self.validate_types!(value, *types, context:)
|
|
16
|
+
return if value.nil? || types.any? { |type| value.is_a?(type) }
|
|
14
17
|
|
|
15
18
|
raise ArgumentError,
|
|
16
|
-
"Expected #{context} to be in "\
|
|
19
|
+
"Expected #{context} to be in " \
|
|
17
20
|
"[#{types.map(&:to_s).join(', ')}], got #{value.class}."
|
|
18
21
|
end
|
|
22
|
+
|
|
23
|
+
# Validate a value is present and not nil.
|
|
24
|
+
# @param value [Object] The value to validate.
|
|
25
|
+
# @param context [String] The context of the value being validated.
|
|
26
|
+
# @raise [ArgumentError] Raises when the value is nil.
|
|
27
|
+
def self.validate_required!(value, context:)
|
|
28
|
+
raise ArgumentError, "Expected #{context} to be set." if value.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Validate unknown parameters are not present for a given Struct.
|
|
32
|
+
# @param struct [Struct] The Struct to validate against.
|
|
33
|
+
# @param params [Hash] The parameters to validate.
|
|
34
|
+
# @param context [String] The context of the value being validated.
|
|
35
|
+
# @raise [ArgumentError] Raises when unknown parameters are present.
|
|
36
|
+
def self.validate_unknown!(struct, params, context:)
|
|
37
|
+
unknown = params.keys - struct.members
|
|
38
|
+
return if unknown.empty?
|
|
39
|
+
|
|
40
|
+
unknown = unknown.map { |key| "#{context}[:#{key}]" }
|
|
41
|
+
raise ArgumentError,
|
|
42
|
+
"Unexpected members: [#{unknown.join(', ')}]"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Validate the given value is within the expected range (inclusive).
|
|
46
|
+
# @param value [Object] The value to validate.
|
|
47
|
+
# @param min [Numeric] The minimum that the given value should be.
|
|
48
|
+
# @param max [Numeric] The maximum that the given value should be.
|
|
49
|
+
# @param context [String] The context of the value being validated.
|
|
50
|
+
# @raise [ArgumentError] Raises when the value is not within expected range.
|
|
51
|
+
def self.validate_range!(value, min:, max:, context:)
|
|
52
|
+
return if value.nil? || value.between?(min, max)
|
|
53
|
+
|
|
54
|
+
raise ArgumentError,
|
|
55
|
+
"Expected #{context} to be between " \
|
|
56
|
+
"#{min} to #{max}, got #{value}."
|
|
57
|
+
end
|
|
19
58
|
end
|
|
20
59
|
end
|
|
@@ -6,8 +6,8 @@ module Hearth
|
|
|
6
6
|
module Waiters
|
|
7
7
|
# Abstract Poller used by generated service Waiters. This class handles
|
|
8
8
|
# sending the request and matching input or output.
|
|
9
|
+
# @api private
|
|
9
10
|
class Poller
|
|
10
|
-
# @api private
|
|
11
11
|
def initialize(options = {})
|
|
12
12
|
@operation_name = options[:operation_name]
|
|
13
13
|
@acceptors = options[:acceptors]
|
|
@@ -42,7 +42,7 @@ module Hearth
|
|
|
42
42
|
# @return [Array<Symbol,Response>]
|
|
43
43
|
def call(client, params = {}, options = {})
|
|
44
44
|
begin
|
|
45
|
-
options = options.merge(
|
|
45
|
+
options = options.merge(input_output_interceptor)
|
|
46
46
|
response = client.send(@operation_name, params, options)
|
|
47
47
|
rescue Hearth::ApiError => e
|
|
48
48
|
error = e
|
|
@@ -58,11 +58,10 @@ module Hearth
|
|
|
58
58
|
|
|
59
59
|
private
|
|
60
60
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{ middleware: MiddlewareBuilder.before_send(middleware) }
|
|
61
|
+
def input_output_interceptor
|
|
62
|
+
# get internal details - capture input before send
|
|
63
|
+
interceptor = proc { |context| @input = context.input }
|
|
64
|
+
{ interceptors: [Interceptor.new(read_before_transmit: interceptor)] }
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
def acceptor_matches?(matcher, response, error)
|
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Hearth
|
|
4
4
|
module Waiters
|
|
5
|
+
# A super class error that is raised when a waiter detects a condition
|
|
6
|
+
# where the waiter can never succeed.
|
|
7
|
+
class WaiterFailed < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised when the waiter reached an expected failure state.
|
|
10
|
+
class FailureStateError < WaiterFailed; end
|
|
11
|
+
|
|
12
|
+
# Raised when the waiter has reached the maximum waiting time.
|
|
13
|
+
class MaxWaitTimeExceededError < WaiterFailed; end
|
|
14
|
+
|
|
15
|
+
# Raised when the waiter received an unexpected error.
|
|
16
|
+
class UnexpectedError < WaiterFailed; end
|
|
17
|
+
|
|
5
18
|
# Abstract waiter class with high level logic for polling and waiting.
|
|
19
|
+
# @api private
|
|
6
20
|
class Waiter
|
|
7
|
-
# @api private
|
|
8
21
|
def initialize(options = {})
|
|
9
22
|
unless options[:max_wait_time].is_a?(Integer)
|
|
10
23
|
raise ArgumentError,
|
|
@@ -42,11 +55,11 @@ module Hearth
|
|
|
42
55
|
case state
|
|
43
56
|
when :retry then nil
|
|
44
57
|
when :success then return
|
|
45
|
-
when :failure then raise
|
|
46
|
-
when :error then raise
|
|
58
|
+
when :failure then raise FailureStateError, resp_or_error
|
|
59
|
+
when :error then raise UnexpectedError, resp_or_error
|
|
47
60
|
end
|
|
48
61
|
|
|
49
|
-
raise
|
|
62
|
+
raise MaxWaitTimeExceededError if @one_more_retry
|
|
50
63
|
|
|
51
64
|
delay = delay(n)
|
|
52
65
|
@remaining_time -= delay
|
data/lib/hearth/xml/formatter.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Hearth
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def text_node(node, pad)
|
|
48
|
-
text = node.text
|
|
48
|
+
text = escape(node.text, :text)
|
|
49
49
|
"#{pad}<#{node.name}#{attrs(node)}>#{text}</#{node.name}>#{@eol}"
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -60,9 +60,18 @@ module Hearth
|
|
|
60
60
|
|
|
61
61
|
def attrs(node)
|
|
62
62
|
node.attributes.map do |key, value|
|
|
63
|
-
" #{key}=#{value
|
|
63
|
+
" #{key}=#{escape(value, :attr)}"
|
|
64
64
|
end.join
|
|
65
65
|
end
|
|
66
|
+
|
|
67
|
+
def escape(string, text_or_attr)
|
|
68
|
+
string.to_s
|
|
69
|
+
.encode(xml: text_or_attr)
|
|
70
|
+
.gsub("\u{000D}", '
') # Carriage Return
|
|
71
|
+
.gsub("\u{000A}", '
') # Line Feed
|
|
72
|
+
.gsub("\u{0085}", '…') # Next Line
|
|
73
|
+
.gsub("\u{2028}", '
') # Line Separator
|
|
74
|
+
end
|
|
66
75
|
end
|
|
67
76
|
end
|
|
68
77
|
end
|
data/lib/hearth/xml/node.rb
CHANGED
|
@@ -9,9 +9,9 @@ module Hearth
|
|
|
9
9
|
BOTH_TYPES = 'Nodes may not have both text and child nodes'
|
|
10
10
|
|
|
11
11
|
# @param [String] name
|
|
12
|
-
def initialize(name, *children)
|
|
12
|
+
def initialize(name, *children, attributes: {})
|
|
13
13
|
@name = name
|
|
14
|
-
@attributes =
|
|
14
|
+
@attributes = attributes
|
|
15
15
|
@child_nodes = []
|
|
16
16
|
@child_node_map = {}
|
|
17
17
|
@text = []
|