aws-sdk-core 3.90.0 → 3.93.0

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.
@@ -0,0 +1,98 @@
1
+ module Aws
2
+ module Plugins
3
+ module Retries
4
+
5
+ # @api private
6
+ class ClockSkew
7
+
8
+ CLOCK_SKEW_THRESHOLD = 5 * 60 # five minutes
9
+
10
+ def initialize
11
+ @mutex = Mutex.new
12
+ # clock_corrections are recorded only on errors
13
+ # and only when time difference is greater than the
14
+ # CLOCK_SKEW_THRESHOLD
15
+ @endpoint_clock_corrections = Hash.new(0)
16
+
17
+ # estimated_skew is calculated on every request
18
+ # and is used to estimate a TTL for requests
19
+ @endpoint_estimated_skews = Hash.new(nil)
20
+ end
21
+
22
+ # Gets the clock_correction in seconds to apply to a given endpoint
23
+ # @param endpoint [URI / String]
24
+ def clock_correction(endpoint)
25
+ @mutex.synchronize { @endpoint_clock_corrections[endpoint.to_s] }
26
+ end
27
+
28
+ # The estimated skew factors in any clock skew from
29
+ # the service along with any network latency.
30
+ # This provides a more accurate value for the ttl,
31
+ # which should represent when the client will stop
32
+ # waiting for a request.
33
+ # Estimated Skew should not be used to correct clock skew errors
34
+ # it should only be used to estimate TTL for a request
35
+ def estimated_skew(endpoint)
36
+ @mutex.synchronize { @endpoint_estimated_skews[endpoint.to_s] }
37
+ end
38
+
39
+ # Determines whether a request has clock skew by comparing
40
+ # the current time against the server's time in the response
41
+ # @param context [Seahorse::Client::RequestContext]
42
+ def clock_skewed?(context)
43
+ server_time = server_time(context.http_response)
44
+ !!server_time &&
45
+ (Time.now.utc - server_time).abs > CLOCK_SKEW_THRESHOLD
46
+ end
47
+
48
+ # Called only on clock skew related errors
49
+ # Update the stored clock skew correction value for an endpoint
50
+ # from the server's time in the response
51
+ # @param context [Seahorse::Client::RequestContext]
52
+ def update_clock_correction(context)
53
+ endpoint = context.http_request.endpoint
54
+ now_utc = Time.now.utc
55
+ server_time = server_time(context.http_response)
56
+ if server_time && (now_utc - server_time).abs > CLOCK_SKEW_THRESHOLD
57
+ set_clock_correction(endpoint, server_time - now_utc)
58
+ end
59
+ end
60
+
61
+ # Called for every request
62
+ # Update our estimated clock skew for the endpoint
63
+ # from the servers time in the response
64
+ # @param context [Seahorse::Client::RequestContext]
65
+ def update_estimated_skew(context)
66
+ endpoint = context.http_request.endpoint
67
+ now_utc = Time.now.utc
68
+ server_time = server_time(context.http_response)
69
+ return unless server_time
70
+ @mutex.synchronize do
71
+ @endpoint_estimated_skews[endpoint.to_s] = server_time - now_utc
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # @param response [Seahorse::Client::Http::Response:]
78
+ def server_time(response)
79
+ begin
80
+ Time.parse(response.headers['date']).utc
81
+ rescue
82
+ nil
83
+ end
84
+ end
85
+
86
+ # Sets the clock correction for an endpoint
87
+ # @param endpoint [URI / String]
88
+ # @param correction [Number]
89
+ def set_clock_correction(endpoint, correction)
90
+ @mutex.synchronize do
91
+ @endpoint_clock_corrections[endpoint.to_s] = correction
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,142 @@
1
+ module Aws
2
+ module Plugins
3
+ module Retries
4
+ # @api private
5
+ # This class will be obsolete when APIs contain modeled exceptions
6
+ class ErrorInspector
7
+ EXPIRED_CREDS = Set.new(
8
+ [
9
+ 'InvalidClientTokenId', # query services
10
+ 'UnrecognizedClientException', # json services
11
+ 'InvalidAccessKeyId', # s3
12
+ 'AuthFailure', # ec2
13
+ 'InvalidIdentityToken', # sts
14
+ 'ExpiredToken' # route53
15
+ ]
16
+ )
17
+
18
+ THROTTLING_ERRORS = Set.new(
19
+ [
20
+ 'Throttling', # query services
21
+ 'ThrottlingException', # json services
22
+ 'ThrottledException', # sns
23
+ 'RequestThrottled', # sqs
24
+ 'RequestThrottledException', # generic service
25
+ 'ProvisionedThroughputExceededException', # dynamodb
26
+ 'TransactionInProgressException', # dynamodb
27
+ 'RequestLimitExceeded', # ec2
28
+ 'BandwidthLimitExceeded', # cloud search
29
+ 'LimitExceededException', # kinesis
30
+ 'TooManyRequestsException', # batch
31
+ 'PriorRequestNotComplete', # route53
32
+ 'SlowDown', # s3
33
+ 'EC2ThrottledException' # ec2
34
+ ]
35
+ )
36
+
37
+ CHECKSUM_ERRORS = Set.new(
38
+ [
39
+ 'CRC32CheckFailed' # dynamodb
40
+ ]
41
+ )
42
+
43
+ NETWORKING_ERRORS = Set.new(
44
+ [
45
+ 'RequestTimeout', # s3
46
+ 'RequestTimeoutException', # glacier
47
+ 'IDPCommunicationError' # sts
48
+ ]
49
+ )
50
+
51
+ # See: https://github.com/aws/aws-sdk-net/blob/5810dfe401e0eac2e59d02276d4b479224b4538e/sdk/src/Core/Amazon.Runtime/Pipeline/RetryHandler/RetryPolicy.cs#L78
52
+ CLOCK_SKEW_ERRORS = Set.new(
53
+ [
54
+ 'RequestTimeTooSkewed',
55
+ 'RequestExpired',
56
+ 'InvalidSignatureException',
57
+ 'SignatureDoesNotMatch',
58
+ 'AuthFailure',
59
+ 'RequestInTheFuture'
60
+ ]
61
+ )
62
+
63
+ def initialize(error, http_status_code)
64
+ @error = error
65
+ @name = extract_name(@error)
66
+ @http_status_code = http_status_code
67
+ end
68
+
69
+ def expired_credentials?
70
+ !!(EXPIRED_CREDS.include?(@name) || @name.match(/expired/i))
71
+ end
72
+
73
+ def throttling_error?
74
+ !!(THROTTLING_ERRORS.include?(@name) ||
75
+ @name.match(/throttl/i) ||
76
+ @http_status_code == 429) ||
77
+ modeled_throttling?
78
+ end
79
+
80
+ def checksum?
81
+ CHECKSUM_ERRORS.include?(@name) || @error.is_a?(Errors::ChecksumError)
82
+ end
83
+
84
+ def networking?
85
+ @error.is_a?(Seahorse::Client::NetworkingError) ||
86
+ @error.is_a?(Errors::NoSuchEndpointError) ||
87
+ NETWORKING_ERRORS.include?(@name)
88
+ end
89
+
90
+ def server?
91
+ (500..599).cover?(@http_status_code)
92
+ end
93
+
94
+ def endpoint_discovery?(context)
95
+ return false unless context.operation.endpoint_discovery
96
+
97
+ @http_status_code == 421 ||
98
+ @name == 'InvalidEndpointException' ||
99
+ @error.is_a?(Errors::EndpointDiscoveryError)
100
+ end
101
+
102
+ def modeled_retryable?
103
+ @error.is_a?(Errors::ServiceError) && @error.retryable?
104
+ end
105
+
106
+ def modeled_throttling?
107
+ @error.is_a?(Errors::ServiceError) && @error.throttling?
108
+ end
109
+
110
+ def clock_skew?(context)
111
+ CLOCK_SKEW_ERRORS.include?(@name) &&
112
+ context.config.clock_skew.clock_skewed?(context)
113
+ end
114
+
115
+ def retryable?(context)
116
+ server? ||
117
+ modeled_retryable? ||
118
+ throttling_error? ||
119
+ networking? ||
120
+ checksum? ||
121
+ endpoint_discovery?(context) ||
122
+ (expired_credentials? && refreshable_credentials?(context)) ||
123
+ clock_skew?(context)
124
+ end
125
+
126
+ private
127
+
128
+ def refreshable_credentials?(context)
129
+ context.config.credentials.respond_to?(:refresh!)
130
+ end
131
+
132
+ def extract_name(error)
133
+ if error.is_a?(Errors::ServiceError)
134
+ error.class.code || error.class.name.to_s
135
+ else
136
+ error.class.name.to_s
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,57 @@
1
+ module Aws
2
+ module Plugins
3
+ module Retries
4
+
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(opts = {})
14
+ @mutex = Mutex.new
15
+ @max_capacity = opts.fetch(:max_capacity, INITIAL_RETRY_TOKENS)
16
+ @available_capacity = @max_capacity
17
+ end
18
+
19
+ # check if there is sufficient capacity to retry
20
+ # and return it. If there is insufficient capacity
21
+ # return 0
22
+ # @return [Integer] The amount of capacity checked out
23
+ def checkout_capacity(error_inspector)
24
+ @mutex.synchronize do
25
+ capacity_amount = if error_inspector.networking?
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
57
+ end
@@ -1,13 +1,17 @@
1
1
  require 'set'
2
+ require_relative 'retries/error_inspector'
3
+ require_relative 'retries/retry_quota'
4
+ require_relative 'retries/client_rate_limiter'
5
+ require_relative 'retries/clock_skew'
2
6
 
3
7
  module Aws
4
8
  module Plugins
5
9
  # @api private
6
10
  class RetryErrors < Seahorse::Client::Plugin
7
-
8
- EQUAL_JITTER = lambda { |delay| (delay / 2) + Kernel.rand(0..(delay/2))}
9
- FULL_JITTER = lambda { |delay| Kernel.rand(0..delay) }
10
- NO_JITTER = lambda { |delay| delay }
11
+ # BEGIN LEGACY OPTIONS
12
+ EQUAL_JITTER = ->(delay) { (delay / 2) + Kernel.rand(0..(delay / 2)) }
13
+ FULL_JITTER = ->(delay) { Kernel.rand(0..delay) }
14
+ NO_JITTER = ->(delay) { delay }
11
15
 
12
16
  JITTERS = {
13
17
  none: NO_JITTER,
@@ -15,168 +19,346 @@ module Aws
15
19
  full: FULL_JITTER
16
20
  }
17
21
 
18
- JITTERS.default_proc = lambda { |h,k|
19
- raise KeyError, "#{k} is not a named jitter function. Must be one of #{h.keys}"
22
+ JITTERS.default_proc = lambda { |h, k|
23
+ raise KeyError,
24
+ "#{k} is not a named jitter function. Must be one of #{h.keys}"
20
25
  }
21
26
 
22
27
  DEFAULT_BACKOFF = lambda do |c|
23
- delay = 2 ** c.retries * c.config.retry_base_delay
24
- delay = [delay, c.config.retry_max_delay].min if (c.config.retry_max_delay || 0) > 0
28
+ delay = 2**c.retries * c.config.retry_base_delay
29
+ if (c.config.retry_max_delay || 0) > 0
30
+ delay = [delay, c.config.retry_max_delay].min
31
+ end
25
32
  jitter = c.config.retry_jitter
26
- jitter = JITTERS[jitter] if Symbol === jitter
33
+ jitter = JITTERS[jitter] if jitter.is_a?(Symbol)
27
34
  delay = jitter.call(delay) if jitter
28
35
  Kernel.sleep(delay)
29
36
  end
30
37
 
31
- option(:retry_limit,
38
+ option(
39
+ :retry_limit,
32
40
  default: 3,
33
41
  doc_type: Integer,
34
42
  docstring: <<-DOCS)
35
43
  The maximum number of times to retry failed requests. Only
36
44
  ~ 500 level server errors and certain ~ 400 level client errors
37
45
  are retried. Generally, these are throttling errors, data
38
- checksum errors, networking errors, timeout errors and auth
39
- errors from expired credentials.
46
+ checksum errors, networking errors, timeout errors, auth errors,
47
+ endpoint discovery, and errors from expired credentials.
48
+ This option is only used in the `legacy` retry mode.
40
49
  DOCS
41
50
 
42
- option(:retry_max_delay,
51
+ option(
52
+ :retry_max_delay,
43
53
  default: 0,
44
54
  doc_type: Integer,
45
55
  docstring: <<-DOCS)
46
- The maximum number of seconds to delay between retries (0 for no limit) used by the default backoff function.
56
+ The maximum number of seconds to delay between retries (0 for no limit)
57
+ used by the default backoff function. This option is only used in the
58
+ `legacy` retry mode.
47
59
  DOCS
48
60
 
49
- option(:retry_base_delay,
61
+ option(
62
+ :retry_base_delay,
50
63
  default: 0.3,
51
64
  doc_type: Float,
52
65
  docstring: <<-DOCS)
53
- The base delay in seconds used by the default backoff function.
66
+ The base delay in seconds used by the default backoff function. This option
67
+ is only used in the `legacy` retry mode.
54
68
  DOCS
55
69
 
56
- option(:retry_jitter,
70
+ option(
71
+ :retry_jitter,
57
72
  default: :none,
58
73
  doc_type: Symbol,
59
74
  docstring: <<-DOCS)
60
- A delay randomiser function used by the default backoff function. Some predefined functions can be referenced by name - :none, :equal, :full, otherwise a Proc that takes and returns a number.
75
+ A delay randomiser function used by the default backoff function.
76
+ Some predefined functions can be referenced by name - :none, :equal, :full,
77
+ otherwise a Proc that takes and returns a number. This option is only used
78
+ in the `legacy` retry mode.
61
79
 
62
80
  @see https://www.awsarchitectureblog.com/2015/03/backoff.html
63
81
  DOCS
64
82
 
65
- option(:retry_backoff, DEFAULT_BACKOFF)
83
+ option(
84
+ :retry_backoff,
85
+ default: DEFAULT_BACKOFF,
86
+ doc_type: Proc,
87
+ docstring: <<-DOCS)
88
+ A proc or lambda used for backoff. Defaults to 2**retries * retry_base_delay.
89
+ This option is only used in the `legacy` retry mode.
90
+ DOCS
66
91
 
67
- # @api private
68
- class ErrorInspector
92
+ # END LEGACY OPTIONS
69
93
 
70
- EXPIRED_CREDS = Set.new([
71
- 'InvalidClientTokenId', # query services
72
- 'UnrecognizedClientException', # json services
73
- 'InvalidAccessKeyId', # s3
74
- 'AuthFailure', # ec2
75
- 'InvalidIdentityToken', # sts
76
- 'ExpiredToken', # route53
77
- ])
94
+ option(
95
+ :retry_mode,
96
+ default: 'legacy',
97
+ doc_type: String,
98
+ docstring: <<-DOCS) do |cfg|
99
+ Specifies which retry algorithm to use. Values are:
78
100
 
79
- THROTTLING_ERRORS = Set.new([
80
- 'Throttling', # query services
81
- 'ThrottlingException', # json services
82
- 'RequestThrottled', # sqs
83
- 'RequestThrottledException',
84
- 'ProvisionedThroughputExceededException', # dynamodb
85
- 'TransactionInProgressException', # dynamodb
86
- 'RequestLimitExceeded', # ec2
87
- 'BandwidthLimitExceeded', # cloud search
88
- 'LimitExceededException', # kinesis
89
- 'TooManyRequestsException', # batch
90
- 'PriorRequestNotComplete', # route53
91
- ])
101
+ * `legacy` - The pre-existing retry behavior. This is default value if
102
+ no retry mode is provided.
92
103
 
93
- CHECKSUM_ERRORS = Set.new([
94
- 'CRC32CheckFailed', # dynamodb
95
- ])
104
+ * `standard` - A standardized set of retry rules across the AWS SDKs.
105
+ This includes support for retry quotas, which limit the number of
106
+ unsuccessful retries a client can make.
96
107
 
97
- NETWORKING_ERRORS = Set.new([
98
- 'RequestTimeout', # s3
99
- 'IDPCommunicationError', # sts
100
- ])
108
+ * `adaptive` - An experimental retry mode that includes all the
109
+ functionality of `standard` mode along with automatic client side
110
+ throttling. This is a provisional mode that may change behavior
111
+ in the future.
101
112
 
102
- def initialize(error, http_status_code)
103
- @error = error
104
- @name = extract_name(error)
105
- @http_status_code = http_status_code
106
- end
113
+ DOCS
114
+ resolve_retry_mode(cfg)
115
+ end
116
+
117
+ option(
118
+ :max_attempts,
119
+ default: 3,
120
+ doc_type: Integer,
121
+ docstring: <<-DOCS) do |cfg|
122
+ An integer representing the maximum number attempts that will be made for
123
+ a single request, including the initial attempt. For example,
124
+ setting this value to 5 will result in a request being retried up to
125
+ 4 times. Used in `standard` and `adaptive` retry modes.
126
+ DOCS
127
+ resolve_max_attempts(cfg)
128
+ end
129
+
130
+ option(
131
+ :adaptive_retry_wait_to_fill,
132
+ default: true,
133
+ doc_type: 'Boolean',
134
+ docstring: <<-DOCS) do |cfg|
135
+ Used only in `adaptive` retry mode. When true, the request will sleep
136
+ until there is sufficent client side capacity to retry the request.
137
+ When false, the request will raise a `RetryCapacityNotAvailableError` and will
138
+ not retry instead of sleeping.
139
+ DOCS
140
+ resolve_adaptive_retry_wait_to_fill(cfg)
141
+ end
142
+
143
+ option(
144
+ :correct_clock_skew,
145
+ default: true,
146
+ doc_type: 'Boolean',
147
+ docstring: <<-DOCS) do |cfg|
148
+ Used only in `standard` and adaptive retry modes. Specifies whether to apply
149
+ a clock skew correction and retry requests with skewed client clocks.
150
+ DOCS
151
+ resolve_correct_clock_skew(cfg)
152
+ end
153
+
154
+ # @api private undocumented
155
+ option(:client_rate_limiter) { Retries::ClientRateLimiter.new }
107
156
 
108
- def expired_credentials?
109
- !!(EXPIRED_CREDS.include?(@name) || @name.match(/expired/i))
157
+ # @api private undocumented
158
+ option(:retry_quota) { Retries::RetryQuota.new }
159
+
160
+ # @api private undocumented
161
+ option(:clock_skew) { Retries::ClockSkew.new }
162
+
163
+ def self.resolve_retry_mode(cfg)
164
+ value = ENV['AWS_RETRY_MODE'] ||
165
+ Aws.shared_config.retry_mode(profile: cfg.profile) ||
166
+ 'legacy'
167
+ # Raise if provided value is not one of the retry modes
168
+ if value != 'legacy' && value != 'standard' && value != 'adaptive'
169
+ raise ArgumentError,
170
+ 'Must provide either `legacy`, `standard`, or `adaptive` for '\
171
+ 'retry_mode profile option or for ENV[\'AWS_RETRY_MODE\']'
110
172
  end
173
+ value
174
+ end
111
175
 
112
- def throttling_error?
113
- !!(THROTTLING_ERRORS.include?(@name) || @name.match(/throttl/i) || @http_status_code == 429)
176
+ def self.resolve_max_attempts(cfg)
177
+ value = ENV['AWS_MAX_ATTEMPTS'] ||
178
+ Aws.shared_config.max_attempts(profile: cfg.profile) ||
179
+ 3
180
+ # Raise if provided value is not a positive integer
181
+ if !value.is_a?(Integer) || value <= 0
182
+ raise ArgumentError,
183
+ 'Must provide a positive integer for max_attempts profile '\
184
+ 'option or for ENV[\'AWS_MAX_ATTEMPTS\']'
114
185
  end
186
+ value
187
+ end
115
188
 
116
- def checksum?
117
- CHECKSUM_ERRORS.include?(@name) || @error.is_a?(Errors::ChecksumError)
189
+ def self.resolve_adaptive_retry_wait_to_fill(cfg)
190
+ value = ENV['AWS_ADAPTIVE_RETRY_WAIT_TO_FILL'] ||
191
+ Aws.shared_config.adaptive_retry_wait_to_fill(profile: cfg.profile) ||
192
+ 'true'
193
+
194
+ # Raise if provided value is not true or false
195
+ if value != 'true' && value != 'false'
196
+ raise ArgumentError,
197
+ 'Must provide either `true` or `false` for '\
198
+ 'adaptive_retry_wait_to_fill profile option or for '\
199
+ 'ENV[\'AWS_ADAPTIVE_RETRY_WAIT_TO_FILL\']'
118
200
  end
119
201
 
120
- def networking?
121
- @error.is_a?(Seahorse::Client::NetworkingError) ||
122
- @error.is_a?(Errors::NoSuchEndpointError) ||
123
- NETWORKING_ERRORS.include?(@name)
202
+ value == 'true'
203
+ end
204
+
205
+ def self.resolve_correct_clock_skew(cfg)
206
+ value = ENV['AWS_CORRECT_CLOCK_SKEW'] ||
207
+ Aws.shared_config.correct_clock_skew(profile: cfg.profile) ||
208
+ 'true'
209
+
210
+ # Raise if provided value is not true or false
211
+ if value != 'true' && value != 'false'
212
+ raise ArgumentError,
213
+ 'Must provide either `true` or `false` for '\
214
+ 'correct_clock_skew profile option or for '\
215
+ 'ENV[\'AWS_CORRECT_CLOCK_SKEW\']'
124
216
  end
125
217
 
126
- def server?
127
- (500..599).include?(@http_status_code)
218
+ value == 'true'
219
+ end
220
+
221
+ class Handler < Seahorse::Client::Handler
222
+ # Max backoff (in seconds)
223
+ MAX_BACKOFF = 20
224
+
225
+ def call(context)
226
+ context.metadata[:retries] ||= {}
227
+ config = context.config
228
+
229
+ get_send_token(config)
230
+ add_retry_headers(context)
231
+ response = @handler.call(context)
232
+ error_inspector = Retries::ErrorInspector.new(
233
+ response.error, response.context.http_response.status_code
234
+ )
235
+
236
+ request_bookkeeping(context, response, error_inspector)
237
+
238
+ if error_inspector.endpoint_discovery?(context)
239
+ key = config.endpoint_cache.extract_key(context)
240
+ config.endpoint_cache.delete(key)
241
+ end
242
+
243
+ # Clock correction needs to be updated from the response even when
244
+ # the request is not retryable but should only be updated
245
+ # in the case of clock skew errors
246
+ if error_inspector.clock_skew?(context)
247
+ config.clock_skew.update_clock_correction(context)
248
+ end
249
+
250
+ # Estimated skew needs to be updated on every request
251
+ config.clock_skew.update_estimated_skew(context)
252
+
253
+ return response unless retryable?(context, response, error_inspector)
254
+
255
+ return response if context.retries >= config.max_attempts - 1
256
+
257
+ context.metadata[:retries][:capacity_amount] =
258
+ config.retry_quota.checkout_capacity(error_inspector)
259
+ return response unless context.metadata[:retries][:capacity_amount] > 0
260
+
261
+ delay = exponential_backoff(context.retries)
262
+ Kernel.sleep(delay)
263
+ retry_request(context, error_inspector)
128
264
  end
129
265
 
130
- def endpoint_discovery?(context)
131
- return false unless context.operation.endpoint_discovery
266
+ private
132
267
 
133
- if @http_status_code == 421 ||
134
- extract_name(@error) == 'InvalidEndpointException'
135
- @error = Errors::EndpointDiscoveryError.new
268
+ def get_send_token(config)
269
+ # either fail fast or block until a token becomes available
270
+ # must be configurable
271
+ # need a maximum rate at which we can send requests (max_send_rate)
272
+ # is unset until a throttle is seen
273
+ if config.retry_mode == 'adaptive'
274
+ config.client_rate_limiter.token_bucket_acquire(
275
+ 1,
276
+ config.adaptive_retry_wait_to_fill
277
+ )
136
278
  end
279
+ end
137
280
 
138
- # When endpoint discovery error occurs
139
- # evict the endpoint from cache
140
- if @error.is_a?(Errors::EndpointDiscoveryError)
141
- key = context.config.endpoint_cache.extract_key(context)
142
- context.config.endpoint_cache.delete(key)
143
- true
144
- else
145
- false
281
+ # maxsendrate is updated if on adaptive mode and based on response
282
+ # retry quota is updated if the request is successful (both modes)
283
+ def request_bookkeeping(context, response, error_inspector)
284
+ config = context.config
285
+ if response.successful?
286
+ config.retry_quota.release(
287
+ context.metadata[:retries][:capacity_amount]
288
+ )
289
+ end
290
+
291
+ if config.retry_mode == 'adaptive'
292
+ is_throttling_error = error_inspector.throttling_error?
293
+ config.client_rate_limiter.update_sending_rate(is_throttling_error)
146
294
  end
147
295
  end
148
296
 
149
- def retryable?(context)
150
- (expired_credentials? and refreshable_credentials?(context)) or
151
- throttling_error? or
152
- checksum? or
153
- networking? or
154
- server? or
155
- endpoint_discovery?(context)
297
+ def retryable?(context, response, error_inspector)
298
+ return false if response.successful?
299
+
300
+ error_inspector.retryable?(context) &&
301
+ context.http_response.body.respond_to?(:truncate)
156
302
  end
157
303
 
158
- private
304
+ def exponential_backoff(retries)
305
+ # for a transient error, use backoff
306
+ [Kernel.rand * 2**retries, MAX_BACKOFF].min
307
+ end
159
308
 
160
- def refreshable_credentials?(context)
161
- context.config.credentials.respond_to?(:refresh!)
309
+ def retry_request(context, error)
310
+ context.retries += 1
311
+ context.config.credentials.refresh! if error.expired_credentials?
312
+ context.http_request.body.rewind
313
+ context.http_response.reset
314
+ call(context)
162
315
  end
163
316
 
164
- def extract_name(error)
165
- if error.is_a?(Errors::ServiceError)
166
- error.class.code
167
- else
168
- error.class.name.to_s
317
+ def add_retry_headers(context)
318
+ request_pairs = {
319
+ 'attempt' => context.retries,
320
+ 'max' => context.config.max_attempts
321
+ }
322
+ if (ttl = compute_request_ttl(context))
323
+ request_pairs['ttl'] = ttl
169
324
  end
325
+
326
+ # create the request header
327
+ formatted_header = request_pairs.map { |k, v| "#{k}=#{v}" }.join('; ')
328
+ context.http_request.headers['amz-sdk-request'] = formatted_header
170
329
  end
171
330
 
331
+ def compute_request_ttl(context)
332
+ return if context.operation.async
333
+
334
+ endpoint = context.http_request.endpoint
335
+ estimated_skew = context.config.clock_skew.estimated_skew(endpoint)
336
+ if context.config.respond_to?(:http_read_timeout)
337
+ read_timeout = context.config.http_read_timeout
338
+ end
339
+
340
+ if estimated_skew && read_timeout
341
+ (Time.now.utc + read_timeout + estimated_skew)
342
+ .strftime('%Y%m%dT%H%M%SZ')
343
+ end
344
+ end
172
345
  end
173
346
 
174
- class Handler < Seahorse::Client::Handler
347
+ class LegacyHandler < Seahorse::Client::Handler
175
348
 
176
349
  def call(context)
177
350
  response = @handler.call(context)
178
351
  if response.error
179
- retry_if_possible(response)
352
+ error_inspector = Retries::ErrorInspector.new(
353
+ response.error, response.context.http_response.status_code
354
+ )
355
+
356
+ if error_inspector.endpoint_discovery?(context)
357
+ key = context.config.endpoint_cache.extract_key(context)
358
+ context.config.endpoint_cache.delete(key)
359
+ end
360
+
361
+ retry_if_possible(response, error_inspector)
180
362
  else
181
363
  response
182
364
  end
@@ -184,21 +366,15 @@ A delay randomiser function used by the default backoff function. Some predefine
184
366
 
185
367
  private
186
368
 
187
- def retry_if_possible(response)
369
+ def retry_if_possible(response, error_inspector)
188
370
  context = response.context
189
- error = error_for(response)
190
- if should_retry?(context, error)
191
- retry_request(context, error)
371
+ if should_retry?(context, error_inspector)
372
+ retry_request(context, error_inspector)
192
373
  else
193
374
  response
194
375
  end
195
376
  end
196
377
 
197
- def error_for(response)
198
- status_code = response.context.http_response.status_code
199
- ErrorInspector.new(response.error, status_code)
200
- end
201
-
202
378
  def retry_request(context, error)
203
379
  delay_retry(context)
204
380
  context.retries += 1
@@ -213,9 +389,9 @@ A delay randomiser function used by the default backoff function. Some predefine
213
389
  end
214
390
 
215
391
  def should_retry?(context, error)
216
- error.retryable?(context) and
217
- context.retries < retry_limit(context) and
218
- response_truncatable?(context)
392
+ error.retryable?(context) &&
393
+ context.retries < retry_limit(context) &&
394
+ response_truncatable?(context)
219
395
  end
220
396
 
221
397
  def retry_limit(context)
@@ -225,15 +401,17 @@ A delay randomiser function used by the default backoff function. Some predefine
225
401
  def response_truncatable?(context)
226
402
  context.http_response.body.respond_to?(:truncate)
227
403
  end
228
-
229
404
  end
230
405
 
231
406
  def add_handlers(handlers, config)
232
- if config.retry_limit > 0
407
+ if config.retry_mode == 'legacy'
408
+ if config.retry_limit > 0
409
+ handlers.add(LegacyHandler, step: :sign, priority: 99)
410
+ end
411
+ else
233
412
  handlers.add(Handler, step: :sign, priority: 99)
234
413
  end
235
414
  end
236
-
237
415
  end
238
416
  end
239
417
  end