smithy-client 1.0.0.pre0 → 1.0.0.pre1
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 +2 -0
- data/VERSION +1 -1
- data/lib/smithy-client/anonymous_provider.rb +12 -0
- data/lib/smithy-client/auth_option.rb +23 -0
- data/lib/smithy-client/auth_scheme.rb +25 -0
- data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
- data/lib/smithy-client/base.rb +200 -0
- data/lib/smithy-client/block_io.rb +36 -0
- data/lib/smithy-client/configuration.rb +222 -0
- data/lib/smithy-client/default_params.rb +91 -0
- data/lib/smithy-client/dynamic_errors.rb +82 -0
- data/lib/smithy-client/endpoint_rules.rb +186 -0
- data/lib/smithy-client/handler.rb +29 -0
- data/lib/smithy-client/handler_builder.rb +33 -0
- data/lib/smithy-client/handler_context.rb +67 -0
- data/lib/smithy-client/handler_list.rb +197 -0
- data/lib/smithy-client/handler_list_entry.rb +102 -0
- data/lib/smithy-client/http/error_inspector.rb +87 -0
- data/lib/smithy-client/http/headers.rb +122 -0
- data/lib/smithy-client/http/request.rb +57 -0
- data/lib/smithy-client/http/response.rb +178 -0
- data/lib/smithy-client/http_api_key_provider.rb +18 -0
- data/lib/smithy-client/http_bearer_provider.rb +18 -0
- data/lib/smithy-client/http_login_provider.rb +19 -0
- data/lib/smithy-client/identities/anonymous.rb +10 -0
- data/lib/smithy-client/identities/http_api_key.rb +18 -0
- data/lib/smithy-client/identities/http_bearer.rb +18 -0
- data/lib/smithy-client/identities/http_login.rb +22 -0
- data/lib/smithy-client/identity.rb +15 -0
- data/lib/smithy-client/log_formatter.rb +215 -0
- data/lib/smithy-client/log_param_filter.rb +88 -0
- data/lib/smithy-client/log_param_formatter.rb +65 -0
- data/lib/smithy-client/managed_file.rb +14 -0
- data/lib/smithy-client/net_http/connection_pool.rb +297 -0
- data/lib/smithy-client/net_http/handler.rb +160 -0
- data/lib/smithy-client/net_http/patches.rb +28 -0
- data/lib/smithy-client/networking_error.rb +16 -0
- data/lib/smithy-client/pageable_response.rb +138 -0
- data/lib/smithy-client/param_converter.rb +243 -0
- data/lib/smithy-client/param_validator.rb +213 -0
- data/lib/smithy-client/plugin.rb +144 -0
- data/lib/smithy-client/plugin_list.rb +141 -0
- data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
- data/lib/smithy-client/plugins/checksum_required.rb +51 -0
- data/lib/smithy-client/plugins/content_length.rb +26 -0
- data/lib/smithy-client/plugins/default_params.rb +22 -0
- data/lib/smithy-client/plugins/host_prefix.rb +69 -0
- data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
- data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
- data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
- data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
- data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
- data/lib/smithy-client/plugins/logging.rb +56 -0
- data/lib/smithy-client/plugins/net_http.rb +163 -0
- data/lib/smithy-client/plugins/pageable_response.rb +37 -0
- data/lib/smithy-client/plugins/param_converter.rb +32 -0
- data/lib/smithy-client/plugins/param_validator.rb +30 -0
- data/lib/smithy-client/plugins/protocol.rb +66 -0
- data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
- data/lib/smithy-client/plugins/request_compression.rb +200 -0
- data/lib/smithy-client/plugins/response_target.rb +71 -0
- data/lib/smithy-client/plugins/retry_errors.rb +125 -0
- data/lib/smithy-client/plugins/sign_requests.rb +24 -0
- data/lib/smithy-client/plugins/stub_responses.rb +102 -0
- data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
- data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
- data/lib/smithy-client/request.rb +76 -0
- data/lib/smithy-client/response.rb +48 -0
- data/lib/smithy-client/retry/adaptive.rb +66 -0
- data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
- data/lib/smithy-client/retry/quota.rb +58 -0
- data/lib/smithy-client/retry/standard.rb +52 -0
- data/lib/smithy-client/retry.rb +36 -0
- data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
- data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
- data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
- data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
- data/lib/smithy-client/service_error.rb +57 -0
- data/lib/smithy-client/signer.rb +16 -0
- data/lib/smithy-client/signers/anonymous.rb +13 -0
- data/lib/smithy-client/signers/http_api_key.rb +52 -0
- data/lib/smithy-client/signers/http_basic.rb +23 -0
- data/lib/smithy-client/signers/http_bearer.rb +19 -0
- data/lib/smithy-client/signers/http_digest.rb +21 -0
- data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
- data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
- data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
- data/lib/smithy-client/stubbing/protocol.rb +29 -0
- data/lib/smithy-client/stubbing/stub_data.rb +25 -0
- data/lib/smithy-client/stubbing.rb +14 -0
- data/lib/smithy-client/stubs.rb +212 -0
- data/lib/smithy-client/util.rb +15 -0
- data/lib/smithy-client/waiters/poller.rb +93 -0
- data/lib/smithy-client/waiters/waiter.rb +113 -0
- data/lib/smithy-client.rb +66 -1
- metadata +163 -9
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
# Represents a request for a service operation call.
|
6
|
+
class Request
|
7
|
+
include HandlerBuilder
|
8
|
+
|
9
|
+
# @option options [HandlerList] :handlers (nil)
|
10
|
+
# @option options [HandlerContext] :context (nil)
|
11
|
+
def initialize(options = {})
|
12
|
+
@handlers = options[:handlers] || HandlerList.new
|
13
|
+
@context = options[:context] || HandlerContext.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [HandlerList]
|
17
|
+
attr_reader :handlers
|
18
|
+
|
19
|
+
# @return [HandlerContext]
|
20
|
+
attr_reader :context
|
21
|
+
|
22
|
+
# Sends the request, returning an {Response} object.
|
23
|
+
#
|
24
|
+
# response = request.send_request
|
25
|
+
#
|
26
|
+
# # Streaming Responses
|
27
|
+
#
|
28
|
+
# By default, responses are buffered into memory. This can be
|
29
|
+
# bad if you are downloading large responses, e.g. large files.
|
30
|
+
# You can avoid this by streaming the response to a block or some other
|
31
|
+
# target.
|
32
|
+
#
|
33
|
+
# ## Streaming to a File
|
34
|
+
#
|
35
|
+
# You can stream the raw response bodies to a File, or any IO-like
|
36
|
+
# object, by passing the `:target` option.
|
37
|
+
#
|
38
|
+
# # create a new file at the given path
|
39
|
+
# request.send_request(target: '/path/to/target/file')
|
40
|
+
#
|
41
|
+
# # or provide an IO object to write to
|
42
|
+
# File.open('photo.jpg', 'wb') do |file|
|
43
|
+
# request.send_request(target: file)
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# **Please Note**: The target IO object may receive `#truncate(0)`
|
47
|
+
# if the request generates a networking error and bytes have already
|
48
|
+
# been written to the target.
|
49
|
+
#
|
50
|
+
# ## Block Streaming
|
51
|
+
#
|
52
|
+
# Pass a block to `#send_request` and the response will be yielded in
|
53
|
+
# chunks to the given block.
|
54
|
+
#
|
55
|
+
# # stream the response data
|
56
|
+
# request.send_request do |chunk|
|
57
|
+
# file.write(chunk)
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# **Please Note**: When streaming to a block, it is not possible to
|
61
|
+
# retry failed requests.
|
62
|
+
#
|
63
|
+
# @option options [String, IO] :target When specified, the response
|
64
|
+
# body is written to the target. This is helpful when you are sending
|
65
|
+
# a request that may return a large payload that you don't want to
|
66
|
+
# load into memory.
|
67
|
+
#
|
68
|
+
# @return [Response, nil]
|
69
|
+
#
|
70
|
+
def send_request(options = {}, &block)
|
71
|
+
@context[:response_target] = options[:target] || block
|
72
|
+
@handlers.to_stack&.call(@context)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module Client
|
7
|
+
# Represents the response for a service operation call.
|
8
|
+
class Response < Delegator
|
9
|
+
# @option options [HandlerContext] :context (nil)
|
10
|
+
# @option options [Structure] :data (nil)
|
11
|
+
# @option options [StandardError] :error (nil)
|
12
|
+
def initialize(options = {})
|
13
|
+
@context = options[:context] || HandlerContext.new
|
14
|
+
@data = options[:data]
|
15
|
+
@error = options[:error]
|
16
|
+
@context.http_response.on_error { |error| @error = error }
|
17
|
+
super(@error || @data)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [HandlerContext]
|
21
|
+
attr_reader :context
|
22
|
+
|
23
|
+
# @return [Structure, nil] The response data. This may be `nil` if the
|
24
|
+
# response contains an {#error}.
|
25
|
+
attr_accessor :data
|
26
|
+
|
27
|
+
# @return [StandardError, nil] The error that occurred during the
|
28
|
+
# operation. This will be `nil` if the operation was successful.
|
29
|
+
attr_accessor :error
|
30
|
+
|
31
|
+
# Necessary to define as a subclass of Delegator
|
32
|
+
# @api private
|
33
|
+
def __getobj__(&)
|
34
|
+
@error || @data
|
35
|
+
end
|
36
|
+
|
37
|
+
# Necessary to define as a subclass of Delegator
|
38
|
+
# @api private
|
39
|
+
def __setobj__(obj)
|
40
|
+
if obj.is_a?(StandardError)
|
41
|
+
@error = obj
|
42
|
+
else
|
43
|
+
@data = obj
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Retry
|
6
|
+
# Adaptive retry strategy for retrying requests.
|
7
|
+
class Adaptive
|
8
|
+
# @option [#call] :backoff (EXPONENTIAL_BACKOFF) A callable object that
|
9
|
+
# calculates a backoff delay for a retry attempt.
|
10
|
+
# @option [Integer] :max_attempts (3) The maximum number of attempts that
|
11
|
+
# will be made for a single request, including the initial attempt.
|
12
|
+
# @option [Boolean] :wait_to_fill When true, the request will sleep until
|
13
|
+
# there is sufficient client side capacity to retry the request. When
|
14
|
+
# false, the request will raise a `CapacityNotAvailableError` and will
|
15
|
+
# not retry instead of sleeping.
|
16
|
+
def initialize(options = {})
|
17
|
+
super()
|
18
|
+
@backoff = options[:backoff] || EXPONENTIAL_BACKOFF
|
19
|
+
@max_attempts = options[:max_attempts] || 3
|
20
|
+
@wait_to_fill = options[:wait_to_fill] || true
|
21
|
+
@client_rate_limiter = ClientRateLimiter.new
|
22
|
+
@quota = Quota.new
|
23
|
+
@capacity_amount = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [#call]
|
27
|
+
attr_reader :backoff
|
28
|
+
|
29
|
+
# @return [Integer]
|
30
|
+
attr_reader :max_attempts
|
31
|
+
|
32
|
+
# @return [Boolean]
|
33
|
+
attr_reader :wait_to_fill
|
34
|
+
|
35
|
+
def acquire_initial_retry_token(_token_scope = nil)
|
36
|
+
@client_rate_limiter.token_bucket_acquire(1, wait_to_fill: @wait_to_fill)
|
37
|
+
Token.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def refresh_retry_token(retry_token, error_info)
|
41
|
+
return unless error_info.retryable?
|
42
|
+
|
43
|
+
@client_rate_limiter.update_sending_rate(
|
44
|
+
error_info.error_type == 'Throttling'
|
45
|
+
)
|
46
|
+
return if retry_token.retry_count >= @max_attempts - 1
|
47
|
+
|
48
|
+
@capacity_amount = @quota.checkout_capacity(error_info)
|
49
|
+
return unless @capacity_amount.positive?
|
50
|
+
|
51
|
+
delay = error_info.hints[:retry_after]
|
52
|
+
delay ||= @backoff.call(retry_token.retry_count)
|
53
|
+
retry_token.retry_count += 1
|
54
|
+
retry_token.retry_delay = delay
|
55
|
+
retry_token
|
56
|
+
end
|
57
|
+
|
58
|
+
def record_success(retry_token)
|
59
|
+
@client_rate_limiter.update_sending_rate(false)
|
60
|
+
@quota.release(@capacity_amount)
|
61
|
+
retry_token
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Retry
|
6
|
+
# Raised when the adaptive retry strategy is unable to acquire capacity.
|
7
|
+
class CapacityNotAvailableError < RuntimeError; end
|
8
|
+
|
9
|
+
# Used only in 'adaptive' retry mode
|
10
|
+
# @api private
|
11
|
+
class ClientRateLimiter
|
12
|
+
MIN_CAPACITY = 1
|
13
|
+
MIN_FILL_RATE = 0.5
|
14
|
+
SMOOTH = 0.8
|
15
|
+
# How much to scale back after a throttling response
|
16
|
+
BETA = 0.7
|
17
|
+
# Controls how aggressively we scale up after being throttled
|
18
|
+
SCALE_CONSTANT = 0.4
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@fill_rate = nil
|
23
|
+
@max_capacity = nil
|
24
|
+
@current_capacity = 0
|
25
|
+
@last_timestamp = nil
|
26
|
+
@enabled = false
|
27
|
+
@measured_tx_rate = 0
|
28
|
+
@last_tx_rate_bucket = monotonic_seconds
|
29
|
+
@request_count = 0
|
30
|
+
@last_max_rate = 0
|
31
|
+
@last_throttle_time = monotonic_seconds
|
32
|
+
@calculated_rate = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def token_bucket_acquire(amount, wait_to_fill: true)
|
36
|
+
# Client side throttling is not enabled until we see a
|
37
|
+
# throttling error
|
38
|
+
return unless @enabled
|
39
|
+
|
40
|
+
@mutex.synchronize do
|
41
|
+
token_bucket_refill
|
42
|
+
|
43
|
+
# Next see if we have enough capacity for the requested amount
|
44
|
+
while @current_capacity < amount
|
45
|
+
raise CapacityNotAvailableError unless wait_to_fill
|
46
|
+
|
47
|
+
@mutex.sleep((amount - @current_capacity) / @fill_rate)
|
48
|
+
token_bucket_refill
|
49
|
+
end
|
50
|
+
@current_capacity -= amount
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_sending_rate(is_throttling_error)
|
55
|
+
@mutex.synchronize do
|
56
|
+
update_measured_rate
|
57
|
+
|
58
|
+
if is_throttling_error
|
59
|
+
# The fill_rate is from the token bucket
|
60
|
+
@last_max_rate = rate_to_use
|
61
|
+
calculate_time_window
|
62
|
+
@last_throttle_time = monotonic_seconds
|
63
|
+
@calculated_rate = cubic_throttle(rate_to_use)
|
64
|
+
@enabled = true
|
65
|
+
else
|
66
|
+
calculate_time_window
|
67
|
+
@calculated_rate = cubic_success(monotonic_seconds)
|
68
|
+
end
|
69
|
+
|
70
|
+
new_rate = [@calculated_rate, 2 * @measured_tx_rate].min
|
71
|
+
token_bucket_update_rate(new_rate)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def monotonic_seconds
|
78
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
79
|
+
end
|
80
|
+
|
81
|
+
def token_bucket_refill
|
82
|
+
timestamp = monotonic_seconds
|
83
|
+
unless @last_timestamp
|
84
|
+
@last_timestamp = timestamp
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
fill_amount = (timestamp - @last_timestamp) * @fill_rate
|
89
|
+
@current_capacity = [@max_capacity, @current_capacity + fill_amount].min
|
90
|
+
@last_timestamp = timestamp
|
91
|
+
end
|
92
|
+
|
93
|
+
def token_bucket_update_rate(new_rps)
|
94
|
+
# Refill based on our current rate before we update to the
|
95
|
+
# new fill rate
|
96
|
+
token_bucket_refill
|
97
|
+
@fill_rate = [new_rps, MIN_FILL_RATE].max
|
98
|
+
@max_capacity = [new_rps, MIN_CAPACITY].max
|
99
|
+
# When we scale down we can't have a current capacity that exceeds our
|
100
|
+
# max_capacity.
|
101
|
+
@current_capacity = [@current_capacity, @max_capacity].min
|
102
|
+
end
|
103
|
+
|
104
|
+
def update_measured_rate
|
105
|
+
t = monotonic_seconds
|
106
|
+
time_bucket = (t * 2).floor / 2.0
|
107
|
+
@request_count += 1
|
108
|
+
return unless time_bucket > @last_tx_rate_bucket
|
109
|
+
|
110
|
+
current_rate = @request_count / (time_bucket - @last_tx_rate_bucket)
|
111
|
+
@measured_tx_rate = (current_rate * SMOOTH) + (@measured_tx_rate * (1 - SMOOTH))
|
112
|
+
@request_count = 0
|
113
|
+
@last_tx_rate_bucket = time_bucket
|
114
|
+
end
|
115
|
+
|
116
|
+
def calculate_time_window
|
117
|
+
# This is broken out into a separate calculation because it only
|
118
|
+
# gets updated when @last_max_rate changes so it can be cached.
|
119
|
+
base = ((@last_max_rate * (1 - BETA)) / SCALE_CONSTANT)
|
120
|
+
@time_window = base**(1.0 / 3)
|
121
|
+
end
|
122
|
+
|
123
|
+
def cubic_success(timestamp)
|
124
|
+
dt = timestamp - @last_throttle_time
|
125
|
+
(SCALE_CONSTANT * ((dt - @time_window)**3)) + @last_max_rate
|
126
|
+
end
|
127
|
+
|
128
|
+
def cubic_throttle(rate_to_use)
|
129
|
+
rate_to_use * BETA
|
130
|
+
end
|
131
|
+
|
132
|
+
def rate_to_use
|
133
|
+
if @enabled
|
134
|
+
[@measured_tx_rate, @fill_rate].min
|
135
|
+
else
|
136
|
+
@measured_tx_rate
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Retry
|
6
|
+
# @api private
|
7
|
+
# Used in 'standard' and 'adaptive' retry modes.
|
8
|
+
class Quota
|
9
|
+
INITIAL_RETRY_TOKENS = 500
|
10
|
+
RETRY_COST = 5
|
11
|
+
NO_RETRY_INCREMENT = 1
|
12
|
+
TIMEOUT_RETRY_COST = 10
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@mutex = Mutex.new
|
16
|
+
@max_capacity = INITIAL_RETRY_TOKENS
|
17
|
+
@available_capacity = @max_capacity
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check if there is sufficient capacity to retry and return it.
|
21
|
+
# If there is insufficient capacity, return 0
|
22
|
+
# @return [Integer] The amount of capacity checked out
|
23
|
+
def checkout_capacity(error_info)
|
24
|
+
@mutex.synchronize do
|
25
|
+
capacity_amount =
|
26
|
+
if error_info.error_type == 'Transient'
|
27
|
+
TIMEOUT_RETRY_COST
|
28
|
+
else
|
29
|
+
RETRY_COST
|
30
|
+
end
|
31
|
+
|
32
|
+
# unable to acquire capacity
|
33
|
+
return 0 if capacity_amount > @available_capacity
|
34
|
+
|
35
|
+
@available_capacity -= capacity_amount
|
36
|
+
capacity_amount
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# capacity_amount refers to the amount of capacity requested from
|
41
|
+
# the last retry. It can either be RETRY_COST, TIMEOUT_RETRY_COST,
|
42
|
+
# or unset.
|
43
|
+
def release(capacity_amount)
|
44
|
+
# Implementation note: The release() method is called for
|
45
|
+
# every API call. In the common case where the request is
|
46
|
+
# successful and we're at full capacity, we can avoid locking.
|
47
|
+
# We can't exceed max capacity so there's no work we have to do.
|
48
|
+
return if @available_capacity == @max_capacity
|
49
|
+
|
50
|
+
@mutex.synchronize do
|
51
|
+
@available_capacity += capacity_amount || NO_RETRY_INCREMENT
|
52
|
+
@available_capacity = [@available_capacity, @max_capacity].min
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Retry
|
6
|
+
# Standard retry strategy for retrying requests.
|
7
|
+
class Standard
|
8
|
+
# @option [#call] :backoff (EXPONENTIAL_BACKOFF) A callable object that
|
9
|
+
# calculates a backoff delay for a retry attempt.
|
10
|
+
# @option [Integer] :max_attempts (3) The maximum number of attempts that
|
11
|
+
# will be made for a single request, including the initial attempt.
|
12
|
+
def initialize(options = {})
|
13
|
+
super()
|
14
|
+
@backoff = options[:backoff] || EXPONENTIAL_BACKOFF
|
15
|
+
@max_attempts = options[:max_attempts] || 3
|
16
|
+
@quota = Quota.new
|
17
|
+
@capacity_amount = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [#call]
|
21
|
+
attr_reader :backoff
|
22
|
+
|
23
|
+
# @return [Integer]
|
24
|
+
attr_reader :max_attempts
|
25
|
+
|
26
|
+
def acquire_initial_retry_token(_token_scope = nil)
|
27
|
+
Token.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def refresh_retry_token(retry_token, error_info)
|
31
|
+
return unless error_info.retryable?
|
32
|
+
|
33
|
+
return if retry_token.retry_count >= @max_attempts - 1
|
34
|
+
|
35
|
+
@capacity_amount = @quota.checkout_capacity(error_info)
|
36
|
+
return unless @capacity_amount.positive?
|
37
|
+
|
38
|
+
delay = error_info.hints[:retry_after]
|
39
|
+
delay ||= @backoff.call(retry_token.retry_count)
|
40
|
+
retry_token.retry_count += 1
|
41
|
+
retry_token.retry_delay = delay
|
42
|
+
retry_token
|
43
|
+
end
|
44
|
+
|
45
|
+
def record_success(retry_token)
|
46
|
+
@quota.release(@capacity_amount)
|
47
|
+
retry_token
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'retry/adaptive'
|
4
|
+
require_relative 'retry/client_rate_limiter'
|
5
|
+
require_relative 'retry/quota'
|
6
|
+
require_relative 'retry/standard'
|
7
|
+
|
8
|
+
module Smithy
|
9
|
+
module Client
|
10
|
+
module Retry
|
11
|
+
# The maximum backoff delay for retrying requests.
|
12
|
+
MAX_BACKOFF = 20
|
13
|
+
|
14
|
+
# The default backoff for retrying requests.
|
15
|
+
EXPONENTIAL_BACKOFF = lambda do |attempts|
|
16
|
+
[Kernel.rand * (2**attempts), MAX_BACKOFF].min || 0
|
17
|
+
end
|
18
|
+
|
19
|
+
# Represents a token that can be used to retry an operation.
|
20
|
+
class Token
|
21
|
+
def initialize
|
22
|
+
@retry_count = 0
|
23
|
+
@retry_delay = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# The number of times the operation has been retried.
|
27
|
+
# @return [Integer]
|
28
|
+
attr_accessor :retry_count
|
29
|
+
|
30
|
+
# The delay before the next retry.
|
31
|
+
# @return [Numeric]
|
32
|
+
attr_accessor :retry_delay
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'request_builder'
|
4
|
+
require_relative 'response_parser'
|
5
|
+
require_relative 'response_stubber'
|
6
|
+
|
7
|
+
module Smithy
|
8
|
+
module Client
|
9
|
+
module RPCv2CBOR
|
10
|
+
# @api private
|
11
|
+
class Protocol
|
12
|
+
def initialize(options = {})
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_request(context)
|
17
|
+
RequestBuilder.new(@options).build(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_error(response)
|
21
|
+
ResponseParser.new(@options).parse_error(response.context)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_data(response)
|
25
|
+
ResponseParser.new(@options).parse_data(response.context)
|
26
|
+
end
|
27
|
+
|
28
|
+
def stub_data(service, operation, data)
|
29
|
+
ResponseStubber.new(@options).stub_data(service, operation, data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def stub_error(service, error_code)
|
33
|
+
ResponseStubber.new(@options).stub_error(service, error_code)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module RPCv2CBOR
|
6
|
+
# @api private
|
7
|
+
class RequestBuilder
|
8
|
+
include Schema::Shapes
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@codec = CBOR::Codec.new(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(context)
|
15
|
+
apply_http_method(context)
|
16
|
+
apply_headers(context)
|
17
|
+
apply_body(context)
|
18
|
+
apply_url_path(context)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def apply_http_method(context)
|
24
|
+
context.http_request.http_method = 'POST'
|
25
|
+
end
|
26
|
+
|
27
|
+
def apply_headers(context)
|
28
|
+
context.http_request.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
|
29
|
+
apply_content_type_header(context)
|
30
|
+
apply_accept_header(context)
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_content_type_header(context)
|
34
|
+
input = context.operation.input
|
35
|
+
content_type =
|
36
|
+
if event_stream?(input)
|
37
|
+
'application/vnd.amazon.eventstream'
|
38
|
+
elsif input.shape != Prelude::Unit
|
39
|
+
'application/cbor'
|
40
|
+
end
|
41
|
+
|
42
|
+
context.http_request.headers['Content-Type'] ||= content_type if content_type
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply_accept_header(context)
|
46
|
+
accept =
|
47
|
+
if event_stream?(context.operation.output)
|
48
|
+
'application/vnd.amazon.eventstream'
|
49
|
+
else
|
50
|
+
'application/cbor'
|
51
|
+
end
|
52
|
+
|
53
|
+
context.http_request.headers['Accept'] ||= accept
|
54
|
+
end
|
55
|
+
|
56
|
+
def apply_body(context)
|
57
|
+
context.http_request.body = @codec.serialize(context.operation.input, context.params)
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_url_path(context)
|
61
|
+
base = context.http_request.endpoint
|
62
|
+
service_name = context.config.service.name
|
63
|
+
base.path += "/service/#{service_name}/operation/#{context.operation.name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def event_stream?(ref)
|
67
|
+
ref.shape.members.each_value do |member_ref|
|
68
|
+
shape = member_ref.shape
|
69
|
+
return true if shape.traits.key?('smithy.api#streaming') && shape.is_a?(UnionShape)
|
70
|
+
end
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module RPCv2CBOR
|
6
|
+
# @api private
|
7
|
+
class ResponseParser
|
8
|
+
def initialize(options = {})
|
9
|
+
@codec = CBOR::Codec.new(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_error(context)
|
13
|
+
if !valid_response?(context)
|
14
|
+
code, message, data = http_status_error(context)
|
15
|
+
build_error(context, code, message, data)
|
16
|
+
elsif (300..599).cover?(context.http_response.status_code)
|
17
|
+
error(context)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_data(context)
|
22
|
+
@codec.deserialize(context.operation.output, context.http_response.body.read)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def valid_response?(context)
|
28
|
+
req_header = context.http_request.headers['smithy-protocol']
|
29
|
+
resp_header = context.http_request.headers['smithy-protocol']
|
30
|
+
req_header == resp_header
|
31
|
+
end
|
32
|
+
|
33
|
+
def error(context)
|
34
|
+
body = context.http_response.body.read
|
35
|
+
if body.empty?
|
36
|
+
code, message, data = http_status_error(context)
|
37
|
+
else
|
38
|
+
code, message, data = extract_error(body, context)
|
39
|
+
end
|
40
|
+
build_error(context, code, message, data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_error(body, context)
|
44
|
+
data = CBOR.decode(body)
|
45
|
+
type = data['__type']
|
46
|
+
code = error_code(type, context)
|
47
|
+
message = data['message']
|
48
|
+
data = parse_error_data(context, body, type)
|
49
|
+
[code, message, data]
|
50
|
+
rescue CBOR::Error
|
51
|
+
[http_status_error_code(context), '', Schema::EmptyStructure.new]
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_error_data(context, body, code)
|
55
|
+
data = Schema::EmptyStructure.new
|
56
|
+
context.operation.errors.each do |ref|
|
57
|
+
next unless ref.shape.id == code
|
58
|
+
|
59
|
+
data = @codec.deserialize(ref, body, ref.shape.type.new)
|
60
|
+
end
|
61
|
+
data
|
62
|
+
end
|
63
|
+
|
64
|
+
def error_code(type, context)
|
65
|
+
return type.split('#').last if type
|
66
|
+
|
67
|
+
http_status_error_code(context)
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_error(context, code, message, data)
|
71
|
+
errors_module = context.client.class.errors_module
|
72
|
+
errors_module.error_class(code).new(context, message, data)
|
73
|
+
end
|
74
|
+
|
75
|
+
def http_status_error(context)
|
76
|
+
[http_status_error_code(context), '', Schema::EmptyStructure.new]
|
77
|
+
end
|
78
|
+
|
79
|
+
def http_status_error_code(context)
|
80
|
+
status_code = context.http_response.status_code
|
81
|
+
"HTTP#{status_code}Error"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|