aws-sdk-core 3.90.1 → 3.91.0

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