Skip to content

Commit 303d99b

Browse files
author
Zelda Hessler
authored
Fix various small issues with the orchestrator (#2736)
## Motivation and Context <!--- Why is this change required? What problem does it solve? --> <!--- If it fixes an open issue, please link to the issue here --> This change fixes many of the smaller issues I ran into during the implementation of standard retries for the orchestrator. Merges from `main` were getting difficult with my [other PR](#2725) so I'm breaking things up. ## Description <!--- Describe your changes in detail --> - when orchestrator attempt timeout occurs, error is now set in context - update test connection to allow defining connection events with optional latency simulation update orchestrator attempt loop to track iteration count - set request attempts from the attempt loop - add comment explaining "rewind" step of making request attempts add `doesnt_matter` method to `TypeErasedBox`, useful when testing update tests to use the new `TypeErasedBox::doesnt_matter` method - add more doc comments - add `set_subsec_nanos` method to `DateTime`. - I added this to make it easier to string-format a datetime that didn't include the nanos. - fix Invocation ID interceptor not inserting the expected header update input type for `OperationError::other` to be more user-friendly - add `test-util` feature to `aws-smithy-runtime-api` - add `test-util` feature to `aws-runtime` - fix presigining inlineable to pull in tower dep during codegen ## Testing <!--- Please describe in detail how you tested your changes --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> tests have been updated where necessary ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 0cba3d8 commit 303d99b

File tree

37 files changed

+457
-236
lines changed

37 files changed

+457
-236
lines changed

aws/rust-runtime/aws-runtime/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ repository = "https://github.com/awslabs/smithy-rs"
99

1010
[features]
1111
event-stream = ["dep:aws-smithy-eventstream", "aws-sigv4/sign-eventstream"]
12+
test-util = []
1213

1314
[dependencies]
1415
aws-credential-types = { path = "../aws-credential-types" }
1516
aws-http = { path = "../aws-http" }
1617
aws-sigv4 = { path = "../aws-sigv4" }
18+
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" }
1719
aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true }
1820
aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" }
1921
aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime" }
2022
aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api" }
2123
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" }
22-
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" }
2324
aws-types = { path = "../aws-types" }
2425
http = "0.2.3"
2526
percent-encoding = "2.1.0"
@@ -28,8 +29,10 @@ uuid = { version = "1", features = ["v4", "fast-rng"] }
2829

2930
[dev-dependencies]
3031
aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] }
31-
aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" }
3232
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] }
33+
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types", features = ["test-util"] }
34+
aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" }
35+
aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["test-util"] }
3336
proptest = "1"
3437
serde = { version = "1", features = ["derive"]}
3538
serde_json = "1"

aws/rust-runtime/aws-runtime/src/invocation_id.rs

Lines changed: 127 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,26 @@ use aws_smithy_runtime_api::client::interceptors::{
99
};
1010
use aws_smithy_types::config_bag::ConfigBag;
1111
use http::{HeaderName, HeaderValue};
12+
use std::fmt::Debug;
1213
use uuid::Uuid;
1314

15+
#[cfg(feature = "test-util")]
16+
pub use test_util::{NoInvocationIdGenerator, PredefinedInvocationIdGenerator};
17+
1418
#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this
1519
const AMZ_SDK_INVOCATION_ID: HeaderName = HeaderName::from_static("amz-sdk-invocation-id");
1620

21+
/// A generator for returning new invocation IDs on demand.
22+
pub trait InvocationIdGenerator: Debug + Send + Sync {
23+
/// Call this function to receive a new [`InvocationId`] or an error explaining why one couldn't
24+
/// be provided.
25+
fn generate(&self) -> Result<Option<InvocationId>, BoxError>;
26+
}
27+
1728
/// This interceptor generates a UUID and attaches it to all request attempts made as part of this operation.
1829
#[non_exhaustive]
19-
#[derive(Debug)]
20-
pub struct InvocationIdInterceptor {
21-
id: InvocationId,
22-
}
30+
#[derive(Debug, Default)]
31+
pub struct InvocationIdInterceptor {}
2332

2433
impl InvocationIdInterceptor {
2534
/// Creates a new `InvocationIdInterceptor`
@@ -28,39 +37,50 @@ impl InvocationIdInterceptor {
2837
}
2938
}
3039

31-
impl Default for InvocationIdInterceptor {
32-
fn default() -> Self {
33-
Self {
34-
id: InvocationId::from_uuid(),
35-
}
36-
}
37-
}
38-
3940
impl Interceptor for InvocationIdInterceptor {
4041
fn modify_before_retry_loop(
4142
&self,
42-
context: &mut BeforeTransmitInterceptorContextMut<'_>,
43-
_cfg: &mut ConfigBag,
43+
_ctx: &mut BeforeTransmitInterceptorContextMut<'_>,
44+
cfg: &mut ConfigBag,
4445
) -> Result<(), BoxError> {
45-
let headers = context.request_mut().headers_mut();
46-
let id = _cfg.get::<InvocationId>().unwrap_or(&self.id);
46+
let id = cfg
47+
.get::<Box<dyn InvocationIdGenerator>>()
48+
.map(|gen| gen.generate())
49+
.transpose()?
50+
.flatten();
51+
cfg.put::<InvocationId>(id.unwrap_or_default());
52+
53+
Ok(())
54+
}
55+
56+
fn modify_before_transmit(
57+
&self,
58+
ctx: &mut BeforeTransmitInterceptorContextMut<'_>,
59+
cfg: &mut ConfigBag,
60+
) -> Result<(), BoxError> {
61+
let headers = ctx.request_mut().headers_mut();
62+
let id = cfg
63+
.get::<InvocationId>()
64+
.ok_or("Expected an InvocationId in the ConfigBag but none was present")?;
4765
headers.append(AMZ_SDK_INVOCATION_ID, id.0.clone());
4866
Ok(())
4967
}
5068
}
5169

5270
/// InvocationId provides a consistent ID across retries
53-
#[derive(Debug)]
71+
#[derive(Debug, Clone, PartialEq, Eq)]
5472
pub struct InvocationId(HeaderValue);
73+
5574
impl InvocationId {
56-
/// A test invocation id to allow deterministic requests
57-
pub fn for_tests() -> Self {
58-
InvocationId(HeaderValue::from_static(
59-
"00000000-0000-4000-8000-000000000000",
60-
))
75+
/// Create a new, random, invocation ID.
76+
pub fn new() -> Self {
77+
Self::default()
6178
}
79+
}
6280

63-
fn from_uuid() -> Self {
81+
/// Defaults to a random UUID.
82+
impl Default for InvocationId {
83+
fn default() -> Self {
6484
let id = Uuid::new_v4();
6585
let id = id
6686
.to_string()
@@ -70,45 +90,107 @@ impl InvocationId {
7090
}
7191
}
7292

93+
#[cfg(feature = "test-util")]
94+
mod test_util {
95+
use super::*;
96+
use std::sync::{Arc, Mutex};
97+
98+
impl InvocationId {
99+
/// Create a new invocation ID from a `&'static str`.
100+
pub fn new_from_str(uuid: &'static str) -> Self {
101+
InvocationId(HeaderValue::from_static(uuid))
102+
}
103+
}
104+
105+
/// A "generator" that returns [`InvocationId`]s from a predefined list.
106+
#[derive(Debug)]
107+
pub struct PredefinedInvocationIdGenerator {
108+
pre_generated_ids: Arc<Mutex<Vec<InvocationId>>>,
109+
}
110+
111+
impl PredefinedInvocationIdGenerator {
112+
/// Given a `Vec<InvocationId>`, create a new [`PredefinedInvocationIdGenerator`].
113+
pub fn new(mut invocation_ids: Vec<InvocationId>) -> Self {
114+
// We're going to pop ids off of the end of the list, so we need to reverse the list or else
115+
// we'll be popping the ids in reverse order, confusing the poor test writer.
116+
invocation_ids.reverse();
117+
118+
Self {
119+
pre_generated_ids: Arc::new(Mutex::new(invocation_ids)),
120+
}
121+
}
122+
}
123+
124+
impl InvocationIdGenerator for PredefinedInvocationIdGenerator {
125+
fn generate(&self) -> Result<Option<InvocationId>, BoxError> {
126+
Ok(Some(
127+
self.pre_generated_ids
128+
.lock()
129+
.expect("this will never be under contention")
130+
.pop()
131+
.expect("testers will provide enough invocation IDs"),
132+
))
133+
}
134+
}
135+
136+
/// A "generator" that always returns `None`.
137+
#[derive(Debug, Default)]
138+
pub struct NoInvocationIdGenerator;
139+
140+
impl NoInvocationIdGenerator {
141+
/// Create a new [`NoInvocationIdGenerator`].
142+
pub fn new() -> Self {
143+
Self::default()
144+
}
145+
}
146+
147+
impl InvocationIdGenerator for NoInvocationIdGenerator {
148+
fn generate(&self) -> Result<Option<InvocationId>, BoxError> {
149+
Ok(None)
150+
}
151+
}
152+
}
153+
73154
#[cfg(test)]
74155
mod tests {
75-
use crate::invocation_id::InvocationIdInterceptor;
156+
use crate::invocation_id::{InvocationId, InvocationIdInterceptor};
76157
use aws_smithy_http::body::SdkBody;
77-
use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext};
158+
use aws_smithy_runtime_api::client::interceptors::{
159+
BeforeTransmitInterceptorContextMut, Interceptor, InterceptorContext,
160+
};
78161
use aws_smithy_types::config_bag::ConfigBag;
79-
use aws_smithy_types::type_erasure::TypedBox;
162+
use aws_smithy_types::type_erasure::TypeErasedBox;
80163
use http::HeaderValue;
81164

82-
fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a HeaderValue {
83-
context
84-
.request()
85-
.expect("request is set")
86-
.headers()
87-
.get(header_name)
88-
.unwrap()
165+
fn expect_header<'a>(
166+
context: &'a BeforeTransmitInterceptorContextMut<'_>,
167+
header_name: &str,
168+
) -> &'a HeaderValue {
169+
context.request().headers().get(header_name).unwrap()
89170
}
90171

91172
#[test]
92173
fn test_id_is_generated_and_set() {
93-
let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase());
94-
context.enter_serialization_phase();
95-
context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
96-
let _ = context.take_input();
97-
context.enter_before_transmit_phase();
174+
let mut ctx = InterceptorContext::new(TypeErasedBox::doesnt_matter());
175+
ctx.enter_serialization_phase();
176+
ctx.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
177+
let _ = ctx.take_input();
178+
ctx.enter_before_transmit_phase();
98179

99-
let mut config = ConfigBag::base();
180+
let mut cfg = ConfigBag::base();
100181
let interceptor = InvocationIdInterceptor::new();
101-
let mut ctx = Into::into(&mut context);
182+
let mut ctx = Into::into(&mut ctx);
102183
interceptor
103-
.modify_before_signing(&mut ctx, &mut config)
184+
.modify_before_retry_loop(&mut ctx, &mut cfg)
104185
.unwrap();
105186
interceptor
106-
.modify_before_retry_loop(&mut ctx, &mut config)
187+
.modify_before_transmit(&mut ctx, &mut cfg)
107188
.unwrap();
108189

109-
let header = expect_header(&context, "amz-sdk-invocation-id");
110-
assert_eq!(&interceptor.id.0, header);
190+
let expected = cfg.get::<InvocationId>().expect("invocation ID was set");
191+
let header = expect_header(&ctx, "amz-sdk-invocation-id");
192+
assert_eq!(expected.0, header, "the invocation ID in the config bag must match the invocation ID in the request header");
111193
// UUID should include 32 chars and 4 dashes
112-
assert_eq!(interceptor.id.0.len(), 36);
194+
assert_eq!(header.len(), 36);
113195
}
114196
}

aws/rust-runtime/aws-runtime/src/recursion_detection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ mod tests {
7575
use aws_smithy_http::body::SdkBody;
7676
use aws_smithy_protocol_test::{assert_ok, validate_headers};
7777
use aws_smithy_runtime_api::client::interceptors::InterceptorContext;
78-
use aws_smithy_types::type_erasure::TypedBox;
78+
use aws_smithy_types::type_erasure::TypeErasedBox;
7979
use aws_types::os_shim_internal::Env;
8080
use http::HeaderValue;
8181
use proptest::{prelude::*, proptest};
@@ -148,7 +148,7 @@ mod tests {
148148
request = request.header(name, value);
149149
}
150150
let request = request.body(SdkBody::empty()).expect("must be valid");
151-
let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase());
151+
let mut context = InterceptorContext::new(TypeErasedBox::doesnt_matter());
152152
context.enter_serialization_phase();
153153
context.set_request(request);
154154
let _ = context.take_input();

aws/rust-runtime/aws-runtime/src/request_info.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
use aws_smithy_runtime::client::orchestrator::interceptors::{RequestAttempts, ServiceClockSkew};
6+
use aws_smithy_runtime::client::orchestrator::interceptors::ServiceClockSkew;
77
use aws_smithy_runtime_api::client::interceptors::{
88
BeforeTransmitInterceptorContextMut, BoxError, Interceptor,
99
};
10+
use aws_smithy_runtime_api::client::request_attempts::RequestAttempts;
1011
use aws_smithy_types::config_bag::ConfigBag;
1112
use aws_smithy_types::date_time::Format;
1213
use aws_smithy_types::retry::RetryConfig;
@@ -44,7 +45,7 @@ impl RequestInfoInterceptor {
4445
let request_attempts = cfg
4546
.get::<RequestAttempts>()
4647
.map(|r_a| r_a.attempts())
47-
.unwrap_or(1);
48+
.unwrap_or(0);
4849
let request_attempts = request_attempts.to_string();
4950
Some((Cow::Borrowed("attempt"), Cow::Owned(request_attempts)))
5051
}
@@ -68,11 +69,19 @@ impl RequestInfoInterceptor {
6869
let estimated_skew: Duration = cfg.get::<ServiceClockSkew>().cloned()?.into();
6970
let current_time = SystemTime::now();
7071
let ttl = current_time.checked_add(socket_read + estimated_skew)?;
71-
let timestamp = DateTime::from(ttl);
72-
let formatted_timestamp = timestamp
72+
let mut timestamp = DateTime::from(ttl);
73+
// Set subsec_nanos to 0 so that the formatted `DateTime` won't have fractional seconds.
74+
timestamp.set_subsec_nanos(0);
75+
let mut formatted_timestamp = timestamp
7376
.fmt(Format::DateTime)
7477
.expect("the resulting DateTime will always be valid");
7578

79+
// Remove dashes and colons
80+
formatted_timestamp = formatted_timestamp
81+
.chars()
82+
.filter(|&c| c != '-' && c != ':')
83+
.collect();
84+
7685
Some((Cow::Borrowed("ttl"), Cow::Owned(formatted_timestamp)))
7786
}
7887
}
@@ -84,13 +93,13 @@ impl Interceptor for RequestInfoInterceptor {
8493
cfg: &mut ConfigBag,
8594
) -> Result<(), BoxError> {
8695
let mut pairs = RequestPairs::new();
87-
if let Some(pair) = self.build_attempts_pair(cfg) {
96+
if let Some(pair) = self.build_ttl_pair(cfg) {
8897
pairs = pairs.with_pair(pair);
8998
}
90-
if let Some(pair) = self.build_max_attempts_pair(cfg) {
99+
if let Some(pair) = self.build_attempts_pair(cfg) {
91100
pairs = pairs.with_pair(pair);
92101
}
93-
if let Some(pair) = self.build_ttl_pair(cfg) {
102+
if let Some(pair) = self.build_max_attempts_pair(cfg) {
94103
pairs = pairs.with_pair(pair);
95104
}
96105

@@ -156,12 +165,11 @@ mod tests {
156165
use super::RequestInfoInterceptor;
157166
use crate::request_info::RequestPairs;
158167
use aws_smithy_http::body::SdkBody;
159-
use aws_smithy_runtime::client::orchestrator::interceptors::RequestAttempts;
160168
use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext};
161169
use aws_smithy_types::config_bag::ConfigBag;
162170
use aws_smithy_types::retry::RetryConfig;
163171
use aws_smithy_types::timeout::TimeoutConfig;
164-
use aws_smithy_types::type_erasure::TypedBox;
172+
use aws_smithy_types::type_erasure::TypeErasedBox;
165173
use http::HeaderValue;
166174
use std::time::Duration;
167175

@@ -178,7 +186,7 @@ mod tests {
178186

179187
#[test]
180188
fn test_request_pairs_for_initial_attempt() {
181-
let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase());
189+
let mut context = InterceptorContext::new(TypeErasedBox::doesnt_matter());
182190
context.enter_serialization_phase();
183191
context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
184192

@@ -189,7 +197,6 @@ mod tests {
189197
.read_timeout(Duration::from_secs(30))
190198
.build(),
191199
);
192-
config.put(RequestAttempts::new());
193200

194201
let _ = context.take_input();
195202
context.enter_before_transmit_phase();

aws/rust-runtime/aws-runtime/src/user_agent.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ mod tests {
112112
use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext};
113113
use aws_smithy_types::config_bag::ConfigBag;
114114
use aws_smithy_types::error::display::DisplayErrorContext;
115-
use aws_smithy_types::type_erasure::TypedBox;
115+
use aws_smithy_types::type_erasure::TypeErasedBox;
116116

117117
fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
118118
context
@@ -126,7 +126,7 @@ mod tests {
126126
}
127127

128128
fn context() -> InterceptorContext {
129-
let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase());
129+
let mut context = InterceptorContext::new(TypeErasedBox::doesnt_matter());
130130
context.enter_serialization_phase();
131131
context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
132132
let _ = context.take_input();

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCustomizableOperationDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class CustomizableOperationTestHelpers(runtimeConfig: RuntimeConfig) :
3333
"SharedInterceptor" to RuntimeType.smithyRuntimeApi(runtimeConfig)
3434
.resolve("client::interceptors::SharedInterceptor"),
3535
"TestParamsSetterInterceptor" to CargoDependency.smithyRuntime(runtimeConfig).withFeature("test-util")
36-
.toType().resolve("client::test_util::interceptor::TestParamsSetterInterceptor"),
36+
.toType().resolve("client::test_util::interceptors::TestParamsSetterInterceptor"),
3737
)
3838

3939
override fun section(section: CustomizableOperationSection): Writable =

0 commit comments

Comments
 (0)