@@ -52,6 +52,8 @@ const DEFAULT_TOKEN_TTL: Duration = Duration::from_secs(21_600);
52
52
const DEFAULT_ATTEMPTS : u32 = 4 ;
53
53
const DEFAULT_CONNECT_TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
54
54
const DEFAULT_READ_TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
55
+ const DEFAULT_OPERATION_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
56
+ const DEFAULT_OPERATION_ATTEMPT_TIMEOUT : Duration = Duration :: from_secs ( 10 ) ;
55
57
56
58
fn user_agent ( ) -> AwsUserAgent {
57
59
AwsUserAgent :: new_from_environment ( Env :: real ( ) , ApiMetadata :: new ( "imds" , PKG_VERSION ) )
@@ -238,6 +240,7 @@ impl ImdsCommonRuntimePlugin {
238
240
config : & ProviderConfig ,
239
241
endpoint_resolver : ImdsEndpointResolver ,
240
242
retry_config : RetryConfig ,
243
+ retry_classifier : SharedRetryClassifier ,
241
244
timeout_config : TimeoutConfig ,
242
245
) -> Self {
243
246
let mut layer = Layer :: new ( "ImdsCommonRuntimePlugin" ) ;
@@ -254,7 +257,7 @@ impl ImdsCommonRuntimePlugin {
254
257
. with_http_client ( config. http_client ( ) )
255
258
. with_endpoint_resolver ( Some ( endpoint_resolver) )
256
259
. with_interceptor ( UserAgentInterceptor :: new ( ) )
257
- . with_retry_classifier ( SharedRetryClassifier :: new ( ImdsResponseRetryClassifier ) )
260
+ . with_retry_classifier ( retry_classifier )
258
261
. with_retry_strategy ( Some ( StandardRetryStrategy :: new ( ) ) )
259
262
. with_time_source ( Some ( config. time_source ( ) ) )
260
263
. with_sleep_impl ( config. sleep_impl ( ) ) ,
@@ -322,7 +325,10 @@ pub struct Builder {
322
325
token_ttl : Option < Duration > ,
323
326
connect_timeout : Option < Duration > ,
324
327
read_timeout : Option < Duration > ,
328
+ operation_timeout : Option < Duration > ,
329
+ operation_attempt_timeout : Option < Duration > ,
325
330
config : Option < ProviderConfig > ,
331
+ retry_classifier : Option < SharedRetryClassifier > ,
326
332
}
327
333
328
334
impl Builder {
@@ -398,6 +404,32 @@ impl Builder {
398
404
self
399
405
}
400
406
407
+ /// Override the operation timeout for IMDS
408
+ ///
409
+ /// This value defaults to 1 second
410
+ pub fn operation_timeout ( mut self , timeout : Duration ) -> Self {
411
+ self . operation_timeout = Some ( timeout) ;
412
+ self
413
+ }
414
+
415
+ /// Override the operation attempt timeout for IMDS
416
+ ///
417
+ /// This value defaults to 1 second
418
+ pub fn operation_attempt_timeout ( mut self , timeout : Duration ) -> Self {
419
+ self . operation_attempt_timeout = Some ( timeout) ;
420
+ self
421
+ }
422
+
423
+ /// Override the retry classifier for IMDS
424
+ ///
425
+ /// This defaults to only retrying on server errors and 401s. The [ImdsResponseRetryClassifier] in this
426
+ /// module offers some configuration options and can be wrapped by[SharedRetryClassifier::new()] for use
427
+ /// here or you can create your own fully customized [SharedRetryClassifier].
428
+ pub fn retry_classifier ( mut self , retry_classifier : SharedRetryClassifier ) -> Self {
429
+ self . retry_classifier = Some ( retry_classifier) ;
430
+ self
431
+ }
432
+
401
433
/* TODO(https://github.com/awslabs/aws-sdk-rust/issues/339): Support customizing the port explicitly */
402
434
/*
403
435
pub fn port(mut self, port: u32) -> Self {
@@ -411,6 +443,11 @@ impl Builder {
411
443
let timeout_config = TimeoutConfig :: builder ( )
412
444
. connect_timeout ( self . connect_timeout . unwrap_or ( DEFAULT_CONNECT_TIMEOUT ) )
413
445
. read_timeout ( self . read_timeout . unwrap_or ( DEFAULT_READ_TIMEOUT ) )
446
+ . operation_attempt_timeout (
447
+ self . operation_attempt_timeout
448
+ . unwrap_or ( DEFAULT_OPERATION_ATTEMPT_TIMEOUT ) ,
449
+ )
450
+ . operation_timeout ( self . operation_timeout . unwrap_or ( DEFAULT_OPERATION_TIMEOUT ) )
414
451
. build ( ) ;
415
452
let endpoint_source = self
416
453
. endpoint
@@ -421,10 +458,14 @@ impl Builder {
421
458
} ;
422
459
let retry_config = RetryConfig :: standard ( )
423
460
. with_max_attempts ( self . max_attempts . unwrap_or ( DEFAULT_ATTEMPTS ) ) ;
461
+ let retry_classifier = self . retry_classifier . unwrap_or ( SharedRetryClassifier :: new (
462
+ ImdsResponseRetryClassifier :: default ( ) ,
463
+ ) ) ;
424
464
let common_plugin = SharedRuntimePlugin :: new ( ImdsCommonRuntimePlugin :: new (
425
465
& config,
426
466
endpoint_resolver,
427
467
retry_config,
468
+ retry_classifier,
428
469
timeout_config,
429
470
) ) ;
430
471
let operation = Operation :: builder ( )
@@ -549,8 +590,20 @@ impl ResolveEndpoint for ImdsEndpointResolver {
549
590
/// - 403 (IMDS disabled): **Not Retryable**
550
591
/// - 404 (Not found): **Not Retryable**
551
592
/// - >=500 (server error): **Retryable**
552
- #[ derive( Clone , Debug ) ]
553
- struct ImdsResponseRetryClassifier ;
593
+ /// - Timeouts: Not retried by default, but this is configurable via [Self::with_retry_connect_timeouts()]
594
+ #[ derive( Clone , Debug , Default ) ]
595
+ #[ non_exhaustive]
596
+ pub struct ImdsResponseRetryClassifier {
597
+ retry_connect_timeouts : bool ,
598
+ }
599
+
600
+ impl ImdsResponseRetryClassifier {
601
+ /// Indicate whether the IMDS client should retry on connection timeouts
602
+ pub fn with_retry_connect_timeouts ( mut self , retry_connect_timeouts : bool ) -> Self {
603
+ self . retry_connect_timeouts = retry_connect_timeouts;
604
+ self
605
+ }
606
+ }
554
607
555
608
impl ClassifyRetry for ImdsResponseRetryClassifier {
556
609
fn name ( & self ) -> & ' static str {
@@ -567,7 +620,10 @@ impl ClassifyRetry for ImdsResponseRetryClassifier {
567
620
// This catch-all includes successful responses that fail to parse. These should not be retried.
568
621
_ => RetryAction :: NoActionIndicated ,
569
622
}
623
+ } else if self . retry_connect_timeouts {
624
+ RetryAction :: server_error ( )
570
625
} else {
626
+ // This is the default behavior.
571
627
// Don't retry timeouts for IMDS, or else it will take ~30 seconds for the default
572
628
// credentials provider chain to fail to provide credentials.
573
629
// Also don't retry non-responses.
@@ -593,7 +649,9 @@ pub(crate) mod test {
593
649
HttpRequest , HttpResponse , OrchestratorError ,
594
650
} ;
595
651
use aws_smithy_runtime_api:: client:: result:: ConnectorError ;
596
- use aws_smithy_runtime_api:: client:: retries:: classifiers:: { ClassifyRetry , RetryAction } ;
652
+ use aws_smithy_runtime_api:: client:: retries:: classifiers:: {
653
+ ClassifyRetry , RetryAction , SharedRetryClassifier ,
654
+ } ;
597
655
use aws_smithy_types:: body:: SdkBody ;
598
656
use aws_smithy_types:: error:: display:: DisplayErrorContext ;
599
657
use aws_types:: os_shim_internal:: { Env , Fs } ;
@@ -603,6 +661,7 @@ pub(crate) mod test {
603
661
use std:: collections:: HashMap ;
604
662
use std:: error:: Error ;
605
663
use std:: io;
664
+ use std:: time:: SystemTime ;
606
665
use std:: time:: { Duration , UNIX_EPOCH } ;
607
666
use tracing_test:: traced_test;
608
667
@@ -933,7 +992,7 @@ pub(crate) mod test {
933
992
let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
934
993
ctx. set_output_or_error ( Ok ( Output :: doesnt_matter ( ) ) ) ;
935
994
ctx. set_response ( imds_response ( "" ) . map ( |_| SdkBody :: empty ( ) ) ) ;
936
- let classifier = ImdsResponseRetryClassifier ;
995
+ let classifier = ImdsResponseRetryClassifier :: default ( ) ;
937
996
assert_eq ! (
938
997
RetryAction :: NoActionIndicated ,
939
998
classifier. classify_retry( & ctx)
@@ -950,6 +1009,65 @@ pub(crate) mod test {
950
1009
) ;
951
1010
}
952
1011
1012
+ /// User provided retry classifier works
1013
+ #[ tokio:: test]
1014
+ async fn user_provided_retry_classifier ( ) {
1015
+ #[ derive( Clone , Debug ) ]
1016
+ struct UserProvidedRetryClassifier ;
1017
+
1018
+ impl ClassifyRetry for UserProvidedRetryClassifier {
1019
+ fn name ( & self ) -> & ' static str {
1020
+ "UserProvidedRetryClassifier"
1021
+ }
1022
+
1023
+ // Don't retry anything
1024
+ fn classify_retry ( & self , _ctx : & InterceptorContext ) -> RetryAction {
1025
+ RetryAction :: RetryForbidden
1026
+ }
1027
+ }
1028
+
1029
+ let events = vec ! [
1030
+ ReplayEvent :: new(
1031
+ token_request( "http://169.254.169.254" , 21600 ) ,
1032
+ token_response( 0 , TOKEN_A ) ,
1033
+ ) ,
1034
+ ReplayEvent :: new(
1035
+ imds_request( "http://169.254.169.254/latest/metadata" , TOKEN_A ) ,
1036
+ http:: Response :: builder( )
1037
+ . status( 401 )
1038
+ . body( SdkBody :: empty( ) )
1039
+ . unwrap( ) ,
1040
+ ) ,
1041
+ ReplayEvent :: new(
1042
+ token_request( "http://169.254.169.254" , 21600 ) ,
1043
+ token_response( 21600 , TOKEN_B ) ,
1044
+ ) ,
1045
+ ReplayEvent :: new(
1046
+ imds_request( "http://169.254.169.254/latest/metadata" , TOKEN_B ) ,
1047
+ imds_response( "ok" ) ,
1048
+ ) ,
1049
+ ] ;
1050
+ let http_client = StaticReplayClient :: new ( events) ;
1051
+
1052
+ let imds_client = super :: Client :: builder ( )
1053
+ . configure (
1054
+ & ProviderConfig :: no_configuration ( )
1055
+ . with_sleep_impl ( InstantSleep :: unlogged ( ) )
1056
+ . with_http_client ( http_client. clone ( ) ) ,
1057
+ )
1058
+ . retry_classifier ( SharedRetryClassifier :: new ( UserProvidedRetryClassifier ) )
1059
+ . build ( ) ;
1060
+
1061
+ let res = imds_client
1062
+ . get ( "/latest/metadata" )
1063
+ . await
1064
+ . expect_err ( "Client should error" ) ;
1065
+
1066
+ // Assert that the operation errored on the initial 401 and did not retry and get
1067
+ // the 200 (since the user provided retry classifier never retries)
1068
+ assert_full_error_contains ! ( res, "401" ) ;
1069
+ }
1070
+
953
1071
// since tokens are sent as headers, the tokens need to be valid header values
954
1072
#[ tokio:: test]
955
1073
async fn invalid_token ( ) {
@@ -989,9 +1107,6 @@ pub(crate) mod test {
989
1107
#[ cfg( feature = "rustls" ) ]
990
1108
async fn one_second_connect_timeout ( ) {
991
1109
use crate :: imds:: client:: ImdsError ;
992
- use aws_smithy_types:: error:: display:: DisplayErrorContext ;
993
- use std:: time:: SystemTime ;
994
-
995
1110
let client = Client :: builder ( )
996
1111
// 240.* can never be resolved
997
1112
. endpoint ( "http://240.0.0.0" )
@@ -1023,6 +1138,40 @@ pub(crate) mod test {
1023
1138
) ;
1024
1139
}
1025
1140
1141
+ /// Retry classifier properly retries timeouts when configured to (meaning it takes ~30s to fail)
1142
+ #[ tokio:: test]
1143
+ async fn retry_connect_timeouts ( ) {
1144
+ let http_client = StaticReplayClient :: new ( vec ! [ ] ) ;
1145
+ let imds_client = super :: Client :: builder ( )
1146
+ . retry_classifier ( SharedRetryClassifier :: new (
1147
+ ImdsResponseRetryClassifier :: default ( ) . with_retry_connect_timeouts ( true ) ,
1148
+ ) )
1149
+ . configure ( & ProviderConfig :: no_configuration ( ) . with_http_client ( http_client. clone ( ) ) )
1150
+ . operation_timeout ( Duration :: from_secs ( 1 ) )
1151
+ . endpoint ( "http://240.0.0.0" )
1152
+ . expect ( "valid uri" )
1153
+ . build ( ) ;
1154
+
1155
+ let now = SystemTime :: now ( ) ;
1156
+ let _res = imds_client
1157
+ . get ( "/latest/metadata" )
1158
+ . await
1159
+ . expect_err ( "240.0.0.0 will never resolve" ) ;
1160
+ let time_elapsed: Duration = now. elapsed ( ) . unwrap ( ) ;
1161
+
1162
+ assert ! (
1163
+ time_elapsed > Duration :: from_secs( 1 ) ,
1164
+ "time_elapsed should be greater than 1s but was {:?}" ,
1165
+ time_elapsed
1166
+ ) ;
1167
+
1168
+ assert ! (
1169
+ time_elapsed < Duration :: from_secs( 2 ) ,
1170
+ "time_elapsed should be less than 2s but was {:?}" ,
1171
+ time_elapsed
1172
+ ) ;
1173
+ }
1174
+
1026
1175
#[ derive( Debug , Deserialize ) ]
1027
1176
struct ImdsConfigTest {
1028
1177
env : HashMap < String , String > ,
0 commit comments