aws-sdk-core 3.90.1 → 3.91.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.
@@ -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,308 @@ 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
93
+
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:
100
+ * `legacy` - The pre-existing retry behavior. This is default value if
101
+ no retry mode is provided.
102
+ * `standard` - A standardized set of retry rules across the AWS SDKs.
103
+ This includes support for retry quotas, which limit the number of
104
+ unsuccessful retries a client can make.
105
+ * `adaptive` - An experimental retry mode that includes all the
106
+ functionality of `standard` mode along with automatic client side
107
+ throttling. This is a provisional mode that may change behavior
108
+ in the future.
109
+ DOCS
110
+ resolve_retry_mode(cfg)
111
+ end
69
112
 
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
- ])
113
+ option(
114
+ :max_attempts,
115
+ default: 3,
116
+ doc_type: Integer,
117
+ docstring: <<-DOCS) do |cfg|
118
+ An integer representing the maximum number attempts that will be made for
119
+ a single request, including the initial attempt. For example,
120
+ setting this value to 5 will result in a request being retried up to
121
+ 4 times. Used in `standard` and `adaptive` retry modes.
122
+ DOCS
123
+ resolve_max_attempts(cfg)
124
+ end
78
125
 
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
- ])
126
+ option(
127
+ :adaptive_retry_wait_to_fill,
128
+ default: true,
129
+ doc_type: 'Boolean',
130
+ docstring: <<-DOCS) do |cfg|
131
+ Used only in `adaptive` retry mode. When true, the request will sleep
132
+ until there is sufficent client side capacity to retry the request.
133
+ When false, the request will raise a `RetryCapacityNotAvailableError` and will
134
+ not retry instead of sleeping.
135
+ DOCS
136
+ resolve_adaptive_retry_wait_to_fill(cfg)
137
+ end
92
138
 
93
- CHECKSUM_ERRORS = Set.new([
94
- 'CRC32CheckFailed', # dynamodb
95
- ])
139
+ option(
140
+ :correct_clock_skew,
141
+ default: true,
142
+ doc_type: 'Boolean',
143
+ docstring: <<-DOCS) do |cfg|
144
+ Used only in `standard` and adaptive retry modes. Specifies whether to apply
145
+ a clock skew correction and retry requests with skewed client clocks.
146
+ DOCS
147
+ resolve_correct_clock_skew(cfg)
148
+ end
96
149
 
97
- NETWORKING_ERRORS = Set.new([
98
- 'RequestTimeout', # s3
99
- 'IDPCommunicationError', # sts
100
- ])
150
+ # @api private undocumented
151
+ option(:client_rate_limiter) { Retries::ClientRateLimiter.new }
101
152
 
102
- def initialize(error, http_status_code)
103
- @error = error
104
- @name = extract_name(error)
105
- @http_status_code = http_status_code
106
- end
153
+ # @api private undocumented
154
+ option(:retry_quota) { Retries::RetryQuota.new }
107
155
 
108
- def expired_credentials?
109
- !!(EXPIRED_CREDS.include?(@name) || @name.match(/expired/i))
110
- end
156
+ # @api private undocumented
157
+ option(:clock_skew) { Retries::ClockSkew.new }
111
158
 
112
- def throttling_error?
113
- !!(THROTTLING_ERRORS.include?(@name) || @name.match(/throttl/i) || @http_status_code == 429)
159
+ def self.resolve_retry_mode(cfg)
160
+ value = ENV['AWS_RETRY_MODE'] ||
161
+ Aws.shared_config.retry_mode(profile: cfg.profile) ||
162
+ 'legacy'
163
+ # Raise if provided value is not one of the retry modes
164
+ if value != 'legacy' && value != 'standard' && value != 'adaptive'
165
+ raise ArgumentError,
166
+ 'Must provide either `legacy`, `standard`, or `adaptive` for '\
167
+ 'retry_mode profile option or for ENV[\'AWS_RETRY_MODE\']'
114
168
  end
169
+ value
170
+ end
115
171
 
116
- def checksum?
117
- CHECKSUM_ERRORS.include?(@name) || @error.is_a?(Errors::ChecksumError)
172
+ def self.resolve_max_attempts(cfg)
173
+ value = ENV['AWS_MAX_ATTEMPTS'] ||
174
+ Aws.shared_config.max_attempts(profile: cfg.profile) ||
175
+ 3
176
+ # Raise if provided value is not a positive integer
177
+ if !value.is_a?(Integer) || value <= 0
178
+ raise ArgumentError,
179
+ 'Must provide a positive integer for max_attempts profile '\
180
+ 'option or for ENV[\'AWS_MAX_ATTEMPTS\']'
118
181
  end
182
+ value
183
+ end
119
184
 
120
- def networking?
121
- @error.is_a?(Seahorse::Client::NetworkingError) ||
122
- @error.is_a?(Errors::NoSuchEndpointError) ||
123
- NETWORKING_ERRORS.include?(@name)
185
+ def self.resolve_adaptive_retry_wait_to_fill(cfg)
186
+ value = ENV['AWS_ADAPTIVE_RETRY_WAIT_TO_FILL'] ||
187
+ Aws.shared_config.adaptive_retry_wait_to_fill(profile: cfg.profile) ||
188
+ 'true'
189
+
190
+ # Raise if provided value is not true or false
191
+ if value != 'true' && value != 'false'
192
+ raise ArgumentError,
193
+ 'Must provide either `true` or `false` for '\
194
+ 'adaptive_retry_wait_to_fill profile option or for '\
195
+ 'ENV[\'AWS_ADAPTIVE_RETRY_WAIT_TO_FILL\']'
124
196
  end
125
197
 
126
- def server?
127
- (500..599).include?(@http_status_code)
198
+ value == 'true'
199
+ end
200
+
201
+ def self.resolve_correct_clock_skew(cfg)
202
+ value = ENV['AWS_CORRECT_CLOCK_SKEW'] ||
203
+ Aws.shared_config.correct_clock_skew(profile: cfg.profile) ||
204
+ 'true'
205
+
206
+ # Raise if provided value is not true or false
207
+ if value != 'true' && value != 'false'
208
+ raise ArgumentError,
209
+ 'Must provide either `true` or `false` for '\
210
+ 'correct_clock_skew profile option or for '\
211
+ 'ENV[\'AWS_CORRECT_CLOCK_SKEW\']'
128
212
  end
129
213
 
130
- def endpoint_discovery?(context)
131
- return false unless context.operation.endpoint_discovery
214
+ value == 'true'
215
+ end
216
+
217
+ class Handler < Seahorse::Client::Handler
218
+ # Max backoff (in seconds)
219
+ MAX_BACKOFF = 20
220
+
221
+ def call(context)
222
+ context.metadata[:retries] ||= {}
223
+ config = context.config
132
224
 
133
- if @http_status_code == 421 ||
134
- extract_name(@error) == 'InvalidEndpointException'
135
- @error = Errors::EndpointDiscoveryError.new
225
+ get_send_token(config)
226
+ response = @handler.call(context)
227
+ error_inspector = Retries::ErrorInspector.new(
228
+ response.error, response.context.http_response.status_code
229
+ )
230
+
231
+ request_bookkeeping(context, response, error_inspector)
232
+
233
+ if error_inspector.endpoint_discovery?(context)
234
+ key = config.endpoint_cache.extract_key(context)
235
+ config.endpoint_cache.delete(key)
136
236
  end
137
237
 
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
238
+ # Clock skew needs to be updated from the response even when
239
+ # the request is not retryable
240
+ if error_inspector.clock_skew?(context)
241
+ config.clock_skew.update_clock_skew(context)
146
242
  end
147
- end
148
243
 
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)
244
+ return response unless retryable?(context, response, error_inspector)
245
+
246
+ return response if context.retries >= config.max_attempts - 1
247
+
248
+ context.metadata[:retries][:capacity_amount] =
249
+ config.retry_quota.checkout_capacity(error_inspector)
250
+ return response unless context.metadata[:retries][:capacity_amount] > 0
251
+
252
+ delay = exponential_backoff(context.retries)
253
+ Kernel.sleep(delay)
254
+ retry_request(context, error_inspector)
156
255
  end
157
256
 
158
257
  private
159
258
 
160
- def refreshable_credentials?(context)
161
- context.config.credentials.respond_to?(:refresh!)
259
+ def get_send_token(config)
260
+ # either fail fast or block until a token becomes available
261
+ # must be configurable
262
+ # need a maximum rate at which we can send requests (max_send_rate)
263
+ # is unset until a throttle is seen
264
+ if config.retry_mode == 'adaptive'
265
+ config.client_rate_limiter.token_bucket_acquire(
266
+ 1,
267
+ config.adaptive_retry_wait_to_fill
268
+ )
269
+ end
162
270
  end
163
271
 
164
- def extract_name(error)
165
- if error.is_a?(Errors::ServiceError)
166
- error.class.code
167
- else
168
- error.class.name.to_s
272
+ # maxsendrate is updated if on adaptive mode and based on response
273
+ # retry quota is updated if the request is successful (both modes)
274
+ def request_bookkeeping(context, response, error_inspector)
275
+ config = context.config
276
+ if response.successful?
277
+ config.retry_quota.release(
278
+ context.metadata[:retries][:capacity_amount]
279
+ )
280
+ end
281
+
282
+ if config.retry_mode == 'adaptive'
283
+ is_throttling_error = error_inspector.throttling_error?
284
+ config.client_rate_limiter.update_sending_rate(is_throttling_error)
169
285
  end
170
286
  end
171
287
 
288
+ def retryable?(context, response, error_inspector)
289
+ return false if response.successful?
290
+
291
+ error_inspector.retryable?(context) &&
292
+ context.http_response.body.respond_to?(:truncate)
293
+ end
294
+
295
+ def exponential_backoff(retries)
296
+ # for a transient error, use backoff
297
+ [Kernel.rand * 2**retries, MAX_BACKOFF].min
298
+ end
299
+
300
+ def retry_request(context, error)
301
+ context.retries += 1
302
+ context.config.credentials.refresh! if error.expired_credentials?
303
+ context.http_request.body.rewind
304
+ context.http_response.reset
305
+ call(context)
306
+ end
172
307
  end
173
308
 
174
- class Handler < Seahorse::Client::Handler
309
+ class LegacyHandler < Seahorse::Client::Handler
175
310
 
176
311
  def call(context)
177
312
  response = @handler.call(context)
178
313
  if response.error
179
- retry_if_possible(response)
314
+ error_inspector = Retries::ErrorInspector.new(
315
+ response.error, response.context.http_response.status_code
316
+ )
317
+
318
+ if error_inspector.endpoint_discovery?(context)
319
+ key = context.config.endpoint_cache.extract_key(context)
320
+ context.config.endpoint_cache.delete(key)
321
+ end
322
+
323
+ retry_if_possible(response, error_inspector)
180
324
  else
181
325
  response
182
326
  end
@@ -184,21 +328,15 @@ A delay randomiser function used by the default backoff function. Some predefine
184
328
 
185
329
  private
186
330
 
187
- def retry_if_possible(response)
331
+ def retry_if_possible(response, error_inspector)
188
332
  context = response.context
189
- error = error_for(response)
190
- if should_retry?(context, error)
191
- retry_request(context, error)
333
+ if should_retry?(context, error_inspector)
334
+ retry_request(context, error_inspector)
192
335
  else
193
336
  response
194
337
  end
195
338
  end
196
339
 
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
340
  def retry_request(context, error)
203
341
  delay_retry(context)
204
342
  context.retries += 1
@@ -213,9 +351,9 @@ A delay randomiser function used by the default backoff function. Some predefine
213
351
  end
214
352
 
215
353
  def should_retry?(context, error)
216
- error.retryable?(context) and
217
- context.retries < retry_limit(context) and
218
- response_truncatable?(context)
354
+ error.retryable?(context) &&
355
+ context.retries < retry_limit(context) &&
356
+ response_truncatable?(context)
219
357
  end
220
358
 
221
359
  def retry_limit(context)
@@ -225,15 +363,17 @@ A delay randomiser function used by the default backoff function. Some predefine
225
363
  def response_truncatable?(context)
226
364
  context.http_response.body.respond_to?(:truncate)
227
365
  end
228
-
229
366
  end
230
367
 
231
368
  def add_handlers(handlers, config)
232
- if config.retry_limit > 0
369
+ if config.retry_mode == 'legacy'
370
+ if config.retry_limit > 0
371
+ handlers.add(LegacyHandler, step: :sign, priority: 99)
372
+ end
373
+ else
233
374
  handlers.add(Handler, step: :sign, priority: 99)
234
375
  end
235
376
  end
236
-
237
377
  end
238
378
  end
239
379
  end