Skip to content

Commit f76afc5

Browse files
authored
Avoid emitting repeated business metrics (#4392)
## Motivation and Context P334648004 ## Description At times, the Rust SDK emits the business metric looking as follows: ``` M/E,b,0,0,0,0,0,0 ``` The issue is due to [the retry loop](https://github.com/smithy-lang/smithy-rs/blob/54f42235c9bce0be0207d5d00a6b35c2501e81d1/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs#L290) in the orchestrator, where any features added to the config bag within `try_attempt` persist throughout subsequent retry iterations (we do have the [rewind](https://github.com/smithy-lang/smithy-rs/blob/54f42235c9bce0be0207d5d00a6b35c2501e81d1/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/context.rs#L367) method but it currently does not touch the config bag at all). This PR uniquifies business metric values emitted in `UserAgentInterceptor`. It also fixes a bug related to the order for emitting `AwsCredentialFeature` where it previously emitted their metric values in a reversed order. ## Dismissed alternative We discussed an alternative approach involving a separate interceptor that runs `modify_before_attempt_completion` that removes `AwsCredentialsFeature` from config bag so the next retry loop can start from the clean slate. This would require extra code, and the approach employed in this PR is simpler. Plus, should the need for the cleanup interceptor arise, it can co-exist with the fix of this PR. ## Testing - CI - Unit tests in `UserAgentInterceptor` - Codegen test in `UserAgentDecoratorTest` ---- _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 54f4223 commit f76afc5

File tree

5 files changed

+195
-21
lines changed

5 files changed

+195
-21
lines changed

aws/codegen-aws-sdk/src/test/kotlin/software/amazon/smithy/rustsdk/UserAgentDecoratorTest.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,82 @@ class UserAgentDecoratorTest {
145145
}
146146
}
147147

148+
@Test
149+
fun `it avoids emitting repeated business metrics on retry`() {
150+
awsSdkIntegrationTest(model) { context, rustCrate ->
151+
rustCrate.integrationTest("business_metrics") {
152+
tokioTest("metrics_should_not_be_repeated") {
153+
val rc = context.runtimeConfig
154+
val moduleName = context.moduleUseName()
155+
rustTemplate(
156+
"""
157+
use $moduleName::config::{AppName, Credentials, Region, SharedCredentialsProvider};
158+
use $moduleName::{Config, Client};
159+
160+
let http_client = #{StaticReplayClient}::new(vec![
161+
#{ReplayEvent}::new(
162+
#{http}::Request::builder()
163+
.uri("http://localhost:1234/")
164+
.body(#{SdkBody}::empty())
165+
.unwrap(),
166+
#{http}::Response::builder()
167+
.status(500)
168+
.body(#{SdkBody}::empty())
169+
.unwrap(),
170+
),
171+
#{ReplayEvent}::new(
172+
#{http}::Request::builder()
173+
.uri("http://localhost:1234/")
174+
.body(#{SdkBody}::empty())
175+
.unwrap(),
176+
#{http}::Response::builder()
177+
.status(200)
178+
.body(#{SdkBody}::empty())
179+
.unwrap(),
180+
),
181+
]);
182+
183+
let mut creds = Credentials::for_tests();
184+
creds.get_property_mut_or_default::<Vec<#{AwsCredentialFeature}>>()
185+
.push(#{AwsCredentialFeature}::CredentialsEnvVars);
186+
187+
let config = Config::builder()
188+
.credentials_provider(SharedCredentialsProvider::new(creds))
189+
.retry_config(#{RetryConfig}::standard().with_max_attempts(2))
190+
.region(Region::new("us-east-1"))
191+
.http_client(http_client.clone())
192+
.app_name(AppName::new("test-app-name").expect("valid app name"))
193+
.build();
194+
let client = Client::from_conf(config);
195+
let _ = client.some_operation().send().await;
196+
197+
let req = http_client.actual_requests().last().unwrap();
198+
let aws_ua_header = req.headers().get("x-amz-user-agent").unwrap();
199+
let metrics_section = aws_ua_header
200+
.split(" m/")
201+
.nth(1)
202+
.unwrap()
203+
.split_ascii_whitespace()
204+
.nth(0)
205+
.unwrap();
206+
assert_eq!(1, metrics_section.matches("g").count());
207+
""",
208+
"http" to RuntimeType.Http,
209+
"AwsCredentialFeature" to
210+
AwsRuntimeType.awsCredentialTypes(rc)
211+
.resolve("credential_feature::AwsCredentialFeature"),
212+
"RetryConfig" to RuntimeType.smithyTypes(rc).resolve("retry::RetryConfig"),
213+
"ReplayEvent" to RuntimeType.smithyHttpClientTestUtil(rc).resolve("test_util::ReplayEvent"),
214+
"SdkBody" to RuntimeType.sdkBody(rc),
215+
"StaticReplayClient" to
216+
RuntimeType.smithyHttpClientTestUtil(rc)
217+
.resolve("test_util::StaticReplayClient"),
218+
)
219+
}
220+
}
221+
}
222+
}
223+
148224
@Test
149225
fun `it emits business metric for RPC v2 CBOR in user agent`() {
150226
val model =

aws/rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-runtime"
3-
version = "1.5.15"
3+
version = "1.5.16"
44
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
55
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
66
edition = "2021"

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

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,36 @@ use crate::sdk_feature::AwsSdkFeature;
2525
use crate::user_agent::metrics::ProvideBusinessMetric;
2626
use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, InvalidMetadataValue};
2727

28+
macro_rules! add_metrics_unique {
29+
($features:expr, $ua:expr, $added:expr) => {
30+
for feature in $features {
31+
if let Some(m) = feature.provide_business_metric() {
32+
if !$added.contains(&m) {
33+
$added.insert(m.clone());
34+
$ua.add_business_metric(m);
35+
}
36+
}
37+
}
38+
};
39+
}
40+
41+
macro_rules! add_metrics_unique_reverse {
42+
($features:expr, $ua:expr, $added:expr) => {
43+
let mut unique_metrics = Vec::new();
44+
for feature in $features {
45+
if let Some(m) = feature.provide_business_metric() {
46+
if !$added.contains(&m) {
47+
$added.insert(m.clone());
48+
unique_metrics.push(m);
49+
}
50+
}
51+
}
52+
for m in unique_metrics.into_iter().rev() {
53+
$ua.add_business_metric(m);
54+
}
55+
};
56+
}
57+
2858
#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this
2959
const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
3060

@@ -133,26 +163,17 @@ impl Intercept for UserAgentInterceptor {
133163
.expect("`AwsUserAgent should have been created in `read_before_execution`")
134164
.clone();
135165

136-
let smithy_sdk_features = cfg.load::<SmithySdkFeature>();
137-
for smithy_sdk_feature in smithy_sdk_features {
138-
smithy_sdk_feature
139-
.provide_business_metric()
140-
.map(|m| ua.add_business_metric(m));
141-
}
166+
let mut added_metrics = std::collections::HashSet::new();
142167

143-
let aws_sdk_features = cfg.load::<AwsSdkFeature>();
144-
for aws_sdk_feature in aws_sdk_features {
145-
aws_sdk_feature
146-
.provide_business_metric()
147-
.map(|m| ua.add_business_metric(m));
148-
}
149-
150-
let aws_credential_features = cfg.load::<AwsCredentialFeature>();
151-
for aws_credential_feature in aws_credential_features {
152-
aws_credential_feature
153-
.provide_business_metric()
154-
.map(|m| ua.add_business_metric(m));
155-
}
168+
add_metrics_unique!(cfg.load::<SmithySdkFeature>(), &mut ua, &mut added_metrics);
169+
add_metrics_unique!(cfg.load::<AwsSdkFeature>(), &mut ua, &mut added_metrics);
170+
// The order we emit credential features matters.
171+
// Reverse to preserve emission order since StoreAppend pops backwards.
172+
add_metrics_unique_reverse!(
173+
cfg.load::<AwsCredentialFeature>(),
174+
&mut ua,
175+
&mut added_metrics
176+
);
156177

157178
let maybe_connector_metadata = runtime_components
158179
.http_client()
@@ -259,6 +280,80 @@ mod tests {
259280
);
260281
}
261282

283+
#[test]
284+
fn test_modify_before_signing_no_duplicate_metrics() {
285+
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
286+
let mut context = context();
287+
288+
let api_metadata = ApiMetadata::new("test-service", "1.0");
289+
let mut layer = Layer::new("test");
290+
layer.store_put(api_metadata);
291+
// Duplicate features
292+
layer.store_append(SmithySdkFeature::Waiter);
293+
layer.store_append(SmithySdkFeature::Waiter);
294+
layer.store_append(AwsSdkFeature::S3Transfer);
295+
layer.store_append(AwsSdkFeature::S3Transfer);
296+
layer.store_append(AwsCredentialFeature::CredentialsCode);
297+
layer.store_append(AwsCredentialFeature::CredentialsCode);
298+
let mut config = ConfigBag::of_layers(vec![layer]);
299+
300+
let interceptor = UserAgentInterceptor::new();
301+
let ctx = Into::into(&context);
302+
interceptor
303+
.read_after_serialization(&ctx, &rc, &mut config)
304+
.unwrap();
305+
let mut ctx = Into::into(&mut context);
306+
interceptor
307+
.modify_before_signing(&mut ctx, &rc, &mut config)
308+
.unwrap();
309+
310+
let aws_ua_header = expect_header(&context, "x-amz-user-agent");
311+
let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
312+
let waiter_count = metrics_section.matches("B").count();
313+
let s3_transfer_count = metrics_section.matches("G").count();
314+
let credentials_code_count = metrics_section.matches("e").count();
315+
assert_eq!(
316+
1, waiter_count,
317+
"Waiter metric should appear only once, but found {waiter_count} occurrences in: {aws_ua_header}",
318+
);
319+
assert_eq!(1, s3_transfer_count, "S3Transfer metric should appear only once, but found {s3_transfer_count} occurrences in metrics section: {aws_ua_header}");
320+
assert_eq!(1, credentials_code_count, "CredentialsCode metric should appear only once, but found {credentials_code_count} occurrences in metrics section: {aws_ua_header}");
321+
}
322+
323+
#[test]
324+
fn test_metrics_order_preserved() {
325+
use aws_credential_types::credential_feature::AwsCredentialFeature;
326+
327+
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
328+
let mut context = context();
329+
330+
let api_metadata = ApiMetadata::new("test-service", "1.0");
331+
let mut layer = Layer::new("test");
332+
layer.store_put(api_metadata);
333+
layer.store_append(AwsCredentialFeature::CredentialsCode);
334+
layer.store_append(AwsCredentialFeature::CredentialsEnvVars);
335+
layer.store_append(AwsCredentialFeature::CredentialsProfile);
336+
let mut config = ConfigBag::of_layers(vec![layer]);
337+
338+
let interceptor = UserAgentInterceptor::new();
339+
let ctx = Into::into(&context);
340+
interceptor
341+
.read_after_serialization(&ctx, &rc, &mut config)
342+
.unwrap();
343+
let mut ctx = Into::into(&mut context);
344+
interceptor
345+
.modify_before_signing(&mut ctx, &rc, &mut config)
346+
.unwrap();
347+
348+
let aws_ua_header = expect_header(&context, "x-amz-user-agent");
349+
let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
350+
351+
assert_eq!(
352+
metrics_section, "e,g,n",
353+
"AwsCredentialFeature metrics should preserve order"
354+
);
355+
}
356+
262357
#[test]
263358
fn test_app_name() {
264359
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
319319

320320
fun smithyHttp(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttp(runtimeConfig).toType()
321321

322+
fun smithyHttpClientTestUtil(runtimeConfig: RuntimeConfig) =
323+
CargoDependency.smithyHttpClientTestUtil(runtimeConfig).toType()
324+
322325
fun smithyJson(runtimeConfig: RuntimeConfig) = CargoDependency.smithyJson(runtimeConfig).toType()
323326

324327
fun smithyQuery(runtimeConfig: RuntimeConfig) = CargoDependency.smithyQuery(runtimeConfig).toType()

0 commit comments

Comments
 (0)