Skip to content

Commit f0ddc66

Browse files
aajtoddZelda Hesslerysaito1001
authored
disable stalled stream protection on empty bodies and after read complete (#3644)
## 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 --> * awslabs/aws-sdk-rust#1141 * awslabs/aws-sdk-rust#1146 * awslabs/aws-sdk-rust#1148 ## Description * Disables stalled stream upload protection for requests with an empty/zero length body. * Disables stalled stream upload throughput checking once the request body has been read and handed off to the HTTP layer. ## Testing Additional integration tests added covering empty bodies and completed uploads. Tested SQS issue against latest runtime and can see it works now. The S3 `CopyObject` issue is related to downloads and will need a different solution. ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [x] I have updated `CHANGELOG.next.toml` if I made changes to the smithy-rs codegen or runtime crates - [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS SDK, generated SDK code, or SDK runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --------- Co-authored-by: Zelda Hessler <zhessler@amazon.com> Co-authored-by: ysaito1001 <awsaito@amazon.com>
1 parent d755bd2 commit f0ddc66

File tree

15 files changed

+339
-49
lines changed

15 files changed

+339
-49
lines changed

CHANGELOG.next.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ references = ["aws-sdk-rust#1079"]
3636
meta = { "breaking" = false, "bug" = true, "tada" = false }
3737
author = "rcoh"
3838

39+
[[aws-sdk-rust]]
40+
message = "Fixes stalled upload stream protection to not apply to empty request bodies and to stop checking for violations once the request body has been read."
41+
references = ["aws-sdk-rust#1141", "aws-sdk-rust#1146", "aws-sdk-rust#1148"]
42+
meta = { "breaking" = false, "tada" = false, "bug" = true }
43+
authors = ["aajtodd", "Velfi"]
44+
45+
[[smithy-rs]]
46+
message = "Fixes stalled upload stream protection to not apply to empty request bodies and to stop checking for violations once the request body has been read."
47+
references = ["aws-sdk-rust#1141", "aws-sdk-rust#1146", "aws-sdk-rust#1148"]
48+
meta = { "breaking" = false, "tada" = false, "bug" = true }
49+
authors = ["aajtodd", "Velfi"]
50+
3951
[[aws-sdk-rust]]
4052
message = "Updating the documentation for the `app_name` method on `ConfigLoader` to indicate the order of precedence for the sources of the `AppName`."
4153
references = ["smithy-rs#3645"]

rust-runtime/aws-smithy-runtime-api/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-smithy-runtime-api"
3-
version = "1.6.0"
3+
version = "1.6.1"
44
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>", "Zelda Hessler <zhessler@amazon.com>"]
55
description = "Smithy runtime types."
66
edition = "2021"

rust-runtime/aws-smithy-runtime-api/src/client/stalled_stream_protection.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
use aws_smithy_types::config_bag::{Storable, StoreReplace};
1414
use std::time::Duration;
1515

16-
const DEFAULT_GRACE_PERIOD: Duration = Duration::from_secs(5);
16+
/// The default grace period for stalled stream protection.
17+
///
18+
/// When a stream stalls for longer than this grace period, the stream will
19+
/// return an error.
20+
pub const DEFAULT_GRACE_PERIOD: Duration = Duration::from_secs(20);
1721

1822
/// Configuration for stalled stream protection.
1923
///

rust-runtime/aws-smithy-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-smithy-runtime"
3-
version = "1.5.2"
3+
version = "1.5.3"
44
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>", "Zelda Hessler <zhessler@amazon.com>"]
55
description = "The new smithy runtime crate"
66
edition = "2021"

rust-runtime/aws-smithy-runtime/src/client/http/body/minimum_throughput.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ impl UploadThroughput {
136136
self.logs.lock().unwrap().push_bytes_transferred(now, bytes);
137137
}
138138

139+
pub(crate) fn mark_complete(&self) -> bool {
140+
self.logs.lock().unwrap().mark_complete()
141+
}
142+
139143
pub(crate) fn report(&self, now: SystemTime) -> ThroughputReport {
140144
self.logs.lock().unwrap().report(now)
141145
}
@@ -177,6 +181,8 @@ trait UploadReport {
177181
impl UploadReport for ThroughputReport {
178182
fn minimum_throughput_violated(self, minimum_throughput: Throughput) -> (bool, Throughput) {
179183
let throughput = match self {
184+
// stream has been exhausted, stop tracking violations
185+
ThroughputReport::Complete => return (false, ZERO_THROUGHPUT),
180186
// If the report is incomplete, then we don't have enough data yet to
181187
// decide if minimum throughput was violated.
182188
ThroughputReport::Incomplete => {

rust-runtime/aws-smithy-runtime/src/client/http/body/minimum_throughput/http_body_0_4_x.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ trait DownloadReport {
2222
impl DownloadReport for ThroughputReport {
2323
fn minimum_throughput_violated(self, minimum_throughput: Throughput) -> (bool, Throughput) {
2424
let throughput = match self {
25+
ThroughputReport::Complete => return (false, ZERO_THROUGHPUT),
2526
// If the report is incomplete, then we don't have enough data yet to
2627
// decide if minimum throughput was violated.
2728
ThroughputReport::Incomplete => {
@@ -175,6 +176,18 @@ where
175176
tracing::trace!("received data: {}", bytes.len());
176177
this.throughput
177178
.push_bytes_transferred(now, bytes.len() as u64);
179+
180+
// hyper will optimistically stop polling when end of stream is reported
181+
// (e.g. when content-length amount of data has been consumed) which means
182+
// we may never get to `Poll:Ready(None)`. Check for same condition and
183+
// attempt to stop checking throughput violations _now_ as we may never
184+
// get polled again. The caveat here is that it depends on `Body` implementations
185+
// implementing `is_end_stream()` correctly. Users can also disable SSP as an
186+
// alternative for such fringe use cases.
187+
if self.is_end_stream() {
188+
tracing::trace!("stream reported end of stream before Poll::Ready(None) reached; marking stream complete");
189+
self.throughput.mark_complete();
190+
}
178191
Poll::Ready(Some(Ok(bytes)))
179192
}
180193
Poll::Pending => {
@@ -183,7 +196,12 @@ where
183196
Poll::Pending
184197
}
185198
// If we've read all the data or an error occurred, then return that result.
186-
res => res,
199+
res => {
200+
if this.throughput.mark_complete() {
201+
tracing::trace!("stream completed: {:?}", res);
202+
}
203+
res
204+
}
187205
}
188206
}
189207

rust-runtime/aws-smithy-runtime/src/client/http/body/minimum_throughput/options.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
*/
55

66
use super::Throughput;
7-
use aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig;
7+
use aws_smithy_runtime_api::client::stalled_stream_protection::{
8+
StalledStreamProtectionConfig, DEFAULT_GRACE_PERIOD,
9+
};
810
use std::time::Duration;
911

10-
/// A collection of options for configuring a [`MinimumThroughputBody`](super::MinimumThroughputBody).
12+
/// A collection of options for configuring a [`MinimumThroughputBody`](super::MinimumThroughputDownloadBody).
1113
#[derive(Debug, Clone)]
1214
pub struct MinimumThroughputBodyOptions {
1315
/// The minimum throughput that is acceptable.
@@ -69,6 +71,13 @@ impl MinimumThroughputBodyOptions {
6971
}
7072
}
7173

74+
const DEFAULT_MINIMUM_THROUGHPUT: Throughput = Throughput {
75+
bytes_read: 1,
76+
per_time_elapsed: Duration::from_secs(1),
77+
};
78+
79+
const DEFAULT_CHECK_WINDOW: Duration = Duration::from_secs(1);
80+
7281
impl Default for MinimumThroughputBodyOptions {
7382
fn default() -> Self {
7483
Self {
@@ -87,14 +96,6 @@ pub struct MinimumThroughputBodyOptionsBuilder {
8796
grace_period: Option<Duration>,
8897
}
8998

90-
const DEFAULT_GRACE_PERIOD: Duration = Duration::from_secs(0);
91-
const DEFAULT_MINIMUM_THROUGHPUT: Throughput = Throughput {
92-
bytes_read: 1,
93-
per_time_elapsed: Duration::from_secs(1),
94-
};
95-
96-
const DEFAULT_CHECK_WINDOW: Duration = Duration::from_secs(1);
97-
9899
impl MinimumThroughputBodyOptionsBuilder {
99100
/// Create a new `MinimumThroughputBodyOptionsBuilder`.
100101
pub fn new() -> Self {

rust-runtime/aws-smithy-runtime/src/client/http/body/minimum_throughput/throughput.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ pub(crate) enum ThroughputReport {
260260
Pending,
261261
/// The stream transferred this amount of throughput during the time window.
262262
Transferred(Throughput),
263+
/// The stream has completed, no more data is expected.
264+
Complete,
263265
}
264266

265267
const BIN_COUNT: usize = 10;
@@ -285,6 +287,7 @@ pub(super) struct ThroughputLogs {
285287
resolution: Duration,
286288
current_tail: SystemTime,
287289
buffer: LogBuffer<BIN_COUNT>,
290+
stream_complete: bool,
288291
}
289292

290293
impl ThroughputLogs {
@@ -302,6 +305,7 @@ impl ThroughputLogs {
302305
resolution,
303306
current_tail: now,
304307
buffer: LogBuffer::new(),
308+
stream_complete: false,
305309
}
306310
}
307311

@@ -343,8 +347,24 @@ impl ThroughputLogs {
343347
assert!(self.current_tail >= now);
344348
}
345349

350+
/// Mark the stream complete indicating no more data is expected. This is an
351+
/// idempotent operation -- subsequent invocations of this function have no effect
352+
/// and return false.
353+
///
354+
/// After marking a stream complete [report](#method.report) will forever more return
355+
/// [ThroughputReport::Complete]
356+
pub(super) fn mark_complete(&mut self) -> bool {
357+
let prev = self.stream_complete;
358+
self.stream_complete = true;
359+
!prev
360+
}
361+
346362
/// Generates an overall report of the time window.
347363
pub(super) fn report(&mut self, now: SystemTime) -> ThroughputReport {
364+
if self.stream_complete {
365+
return ThroughputReport::Complete;
366+
}
367+
348368
self.catch_up(now);
349369
self.buffer.fill_gaps();
350370

rust-runtime/aws-smithy-runtime/src/client/stalled_stream_protection.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ impl Intercept for StalledStreamProtectionInterceptor {
6565
) -> Result<(), BoxError> {
6666
if let Some(sspcfg) = cfg.load::<StalledStreamProtectionConfig>().cloned() {
6767
if sspcfg.upload_enabled() {
68+
if let Some(0) = context.request().body().content_length() {
69+
tracing::trace!(
70+
"skipping stalled stream protection for zero length request body"
71+
);
72+
return Ok(());
73+
}
6874
let (_async_sleep, time_source) = get_runtime_component_deps(runtime_components)?;
6975
let now = time_source.now();
7076

rust-runtime/aws-smithy-runtime/tests/stalled_stream_download.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,24 @@ async fn download_stalls() {
105105
let (time, sleep) = tick_advance_time_and_sleep();
106106
let (server, response_sender) = channel_server();
107107
let op = operation(server, time.clone(), sleep);
108+
let barrier = Arc::new(Barrier::new(2));
108109

110+
let c = barrier.clone();
109111
let server = tokio::spawn(async move {
110-
for _ in 1..10 {
112+
c.wait().await;
113+
for i in 1..10 {
114+
tracing::debug!("send {i}");
111115
response_sender.send(NEAT_DATA).await.unwrap();
112116
tick!(time, Duration::from_secs(1));
113117
}
114118
tick!(time, Duration::from_secs(10));
115119
});
116120

117121
let response_body = op.invoke(()).await.expect("initial success");
118-
let result = tokio::spawn(eagerly_consume(response_body));
122+
let result = tokio::spawn(async move {
123+
barrier.wait().await;
124+
eagerly_consume(response_body).await
125+
});
119126
server.await.unwrap();
120127

121128
let err = result
@@ -188,6 +195,7 @@ async fn user_downloads_data_too_slowly() {
188195
}
189196

190197
use download_test_tools::*;
198+
use tokio::sync::Barrier;
191199
mod download_test_tools {
192200
use crate::stalled_stream_common::*;
193201

0 commit comments

Comments
 (0)