aws-sdk-core 3.90.0 → 3.93.0

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