Skip to content

Commit 2e9ade0

Browse files
committed
Reintroduce overhauled stalled stream protection and upload support
This commit unreverts 27834ae (#3485). Original commit message: Overhaul stalled stream protection and add upload support (#3485) This PR overhauls the existing stalled stream protection with a new algorithm, and also adds support for minimum throughput on upload streams. The new algorithm adds support for differentiating between the user or the server causing the stall, and not timing out if it's the user causing the stall. This will fix timeout issues when a customer makes remote service calls in between streaming pieces of information.
1 parent 170df73 commit 2e9ade0

File tree

21 files changed

+2007
-601
lines changed

21 files changed

+2007
-601
lines changed

CHANGELOG.next.toml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,46 @@
99
# message = "Fix typos in module documentation for generated crates"
1010
# references = ["smithy-rs#920"]
1111
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
12-
# author = "rcoh"
12+
# author = "rcoh"
13+
14+
[[smithy-rs]]
15+
message = """
16+
Stalled stream protection now supports request upload streams. It is currently off by default, but will be enabled by default in a future release. To enable it now, you can do the following:
17+
18+
```rust
19+
let config = my_service::Config::builder()
20+
.stalled_stream_protection(StalledStreamProtectionConfig::enabled().build())
21+
// ...
22+
.build();
23+
```
24+
"""
25+
references = ["smithy-rs#3485"]
26+
meta = { "breaking" = false, "tada" = true, "bug" = false }
27+
authors = ["jdisanti"]
28+
29+
[[aws-sdk-rust]]
30+
message = """
31+
Stalled stream protection now supports request upload streams. It is currently off by default, but will be enabled by default in a future release. To enable it now, you can do the following:
32+
33+
```rust
34+
let config = aws_config::defaults(BehaviorVersion::latest())
35+
.stalled_stream_protection(StalledStreamProtectionConfig::enabled().build())
36+
.load()
37+
.await;
38+
```
39+
"""
40+
references = ["smithy-rs#3485"]
41+
meta = { "breaking" = false, "tada" = true, "bug" = false }
42+
author = "jdisanti"
43+
44+
[[smithy-rs]]
45+
message = "Stalled stream protection on downloads will now only trigger if the upstream source is too slow. Previously, stalled stream protection could be erroneously triggered if the user was slowly consuming the stream slower than the minimum speed limit."
46+
references = ["smithy-rs#3485"]
47+
meta = { "breaking" = false, "tada" = false, "bug" = true }
48+
authors = ["jdisanti"]
49+
50+
[[aws-sdk-rust]]
51+
message = "Stalled stream protection on downloads will now only trigger if the upstream source is too slow. Previously, stalled stream protection could be erroneously triggered if the user was slowly consuming the stream slower than the minimum speed limit."
52+
references = ["smithy-rs#3485"]
53+
meta = { "breaking" = false, "tada" = false, "bug" = true }
54+
author = "jdisanti"

aws/sdk/integration-tests/s3/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@ tracing-subscriber = { version = "0.3.15", features = ["env-filter", "json"] }
4848
# If you're writing a test with this, take heed! `no-env-filter` means you'll be capturing
4949
# logs from everything that speaks, so be specific with your asserts.
5050
tracing-test = { version = "0.2.4", features = ["no-env-filter"] }
51+
52+
[dependencies]
53+
pin-project-lite = "0.2.13"
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
//! Body wrappers must pass through size_hint
7+
8+
use aws_config::SdkConfig;
9+
use aws_sdk_s3::{
10+
config::{Credentials, Region, SharedCredentialsProvider},
11+
primitives::{ByteStream, SdkBody},
12+
Client,
13+
};
14+
use aws_smithy_runtime::client::http::test_util::{capture_request, infallible_client_fn};
15+
use http_body::Body;
16+
17+
#[tokio::test]
18+
async fn download_body_size_hint_check() {
19+
let test_body_content = b"hello";
20+
let test_body = || SdkBody::from(&test_body_content[..]);
21+
assert_eq!(
22+
Some(test_body_content.len() as u64),
23+
(test_body)().size_hint().exact(),
24+
"pre-condition check"
25+
);
26+
27+
let http_client = infallible_client_fn(move |_| {
28+
http::Response::builder()
29+
.status(200)
30+
.body((test_body)())
31+
.unwrap()
32+
});
33+
let sdk_config = SdkConfig::builder()
34+
.credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
35+
.region(Region::new("us-east-1"))
36+
.http_client(http_client)
37+
.build();
38+
let client = Client::new(&sdk_config);
39+
let response = client
40+
.get_object()
41+
.bucket("foo")
42+
.key("foo")
43+
.send()
44+
.await
45+
.unwrap();
46+
assert_eq!(
47+
(
48+
test_body_content.len() as u64,
49+
Some(test_body_content.len() as u64),
50+
),
51+
response.body.size_hint(),
52+
"the size hint should be passed through all the default body wrappers"
53+
);
54+
}
55+
56+
#[tokio::test]
57+
async fn upload_body_size_hint_check() {
58+
let test_body_content = b"hello";
59+
60+
let (http_client, rx) = capture_request(None);
61+
let sdk_config = SdkConfig::builder()
62+
.credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
63+
.region(Region::new("us-east-1"))
64+
.http_client(http_client)
65+
.build();
66+
let client = Client::new(&sdk_config);
67+
let body = ByteStream::from_static(test_body_content);
68+
assert_eq!(
69+
(
70+
test_body_content.len() as u64,
71+
Some(test_body_content.len() as u64),
72+
),
73+
body.size_hint(),
74+
"pre-condition check"
75+
);
76+
let _response = client
77+
.put_object()
78+
.bucket("foo")
79+
.key("foo")
80+
.body(body)
81+
.send()
82+
.await;
83+
let captured_request = rx.expect_request();
84+
assert_eq!(
85+
Some(test_body_content.len() as u64),
86+
captured_request.body().size_hint().exact(),
87+
"the size hint should be passed through all the default body wrappers"
88+
);
89+
}

aws/sdk/integration-tests/s3/tests/stalled-stream-protection.rs

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,119 @@
44
*/
55

66
use aws_credential_types::Credentials;
7-
use aws_sdk_s3::config::{Region, StalledStreamProtectionConfig};
8-
use aws_sdk_s3::primitives::ByteStream;
7+
use aws_sdk_s3::{
8+
config::{Region, StalledStreamProtectionConfig},
9+
error::BoxError,
10+
};
11+
use aws_sdk_s3::{error::DisplayErrorContext, primitives::ByteStream};
912
use aws_sdk_s3::{Client, Config};
10-
use bytes::BytesMut;
13+
use aws_smithy_runtime::{assert_str_contains, test_util::capture_test_logs::capture_test_logs};
14+
use aws_smithy_types::body::SdkBody;
15+
use bytes::{Bytes, BytesMut};
16+
use http_body::Body;
1117
use std::error::Error;
12-
use std::future::Future;
13-
use std::net::SocketAddr;
1418
use std::time::Duration;
19+
use std::{future::Future, task::Poll};
20+
use std::{net::SocketAddr, pin::Pin, task::Context};
21+
use tokio::{
22+
net::{TcpListener, TcpStream},
23+
time::sleep,
24+
};
1525
use tracing::debug;
1626

17-
// This test doesn't work because we can't count on `hyper` to poll the body,
18-
// regardless of whether we schedule a wake. To make this functionality work,
19-
// we'd have to integrate more closely with the orchestrator.
20-
//
21-
// I'll leave this test here because we do eventually want to support stalled
22-
// stream protection for uploads.
23-
#[ignore]
27+
enum SlowBodyState {
28+
Wait(Pin<Box<dyn std::future::Future<Output = ()> + Send + Sync + 'static>>),
29+
Send,
30+
Taken,
31+
}
32+
33+
struct SlowBody {
34+
state: SlowBodyState,
35+
}
36+
37+
impl SlowBody {
38+
fn new() -> Self {
39+
Self {
40+
state: SlowBodyState::Send,
41+
}
42+
}
43+
}
44+
45+
impl Body for SlowBody {
46+
type Data = Bytes;
47+
type Error = BoxError;
48+
49+
fn poll_data(
50+
mut self: Pin<&mut Self>,
51+
cx: &mut Context<'_>,
52+
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
53+
loop {
54+
let mut state = SlowBodyState::Taken;
55+
std::mem::swap(&mut state, &mut self.state);
56+
match state {
57+
SlowBodyState::Wait(mut fut) => match fut.as_mut().poll(cx) {
58+
Poll::Ready(_) => self.state = SlowBodyState::Send,
59+
Poll::Pending => {
60+
self.state = SlowBodyState::Wait(fut);
61+
return Poll::Pending;
62+
}
63+
},
64+
SlowBodyState::Send => {
65+
self.state = SlowBodyState::Wait(Box::pin(sleep(Duration::from_micros(100))));
66+
return Poll::Ready(Some(Ok(Bytes::from_static(
67+
b"data_data_data_data_data_data_data_data_data_data_data_data_\
68+
data_data_data_data_data_data_data_data_data_data_data_data_\
69+
data_data_data_data_data_data_data_data_data_data_data_data_\
70+
data_data_data_data_data_data_data_data_data_data_data_data_",
71+
))));
72+
}
73+
SlowBodyState::Taken => unreachable!(),
74+
}
75+
}
76+
}
77+
78+
fn poll_trailers(
79+
self: Pin<&mut Self>,
80+
_cx: &mut Context<'_>,
81+
) -> Poll<Result<Option<http::HeaderMap>, Self::Error>> {
82+
Poll::Ready(Ok(None))
83+
}
84+
}
85+
2486
#[tokio::test]
2587
async fn test_stalled_stream_protection_defaults_for_upload() {
26-
// We spawn a faulty server that will close the connection after
27-
// writing half of the response body.
88+
let _logs = capture_test_logs();
89+
90+
// We spawn a faulty server that will stop all request processing after reading half of the request body.
2891
let (server, server_addr) = start_faulty_upload_server().await;
2992
let _ = tokio::spawn(server);
3093

3194
let conf = Config::builder()
3295
.credentials_provider(Credentials::for_tests())
3396
.region(Region::new("us-east-1"))
3497
.endpoint_url(format!("http://{server_addr}"))
35-
// .stalled_stream_protection(StalledStreamProtectionConfig::enabled().build())
98+
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3510): make stalled stream protection enabled by default with BMV and remove this line
99+
.stalled_stream_protection(StalledStreamProtectionConfig::enabled().build())
36100
.build();
37101
let client = Client::from_conf(conf);
38102

39103
let err = client
40104
.put_object()
41105
.bucket("a-test-bucket")
42106
.key("stalled-stream-test.txt")
43-
.body(ByteStream::from_static(b"Hello"))
107+
.body(ByteStream::new(SdkBody::from_body_0_4(SlowBody::new())))
44108
.send()
45109
.await
46110
.expect_err("upload stream stalled out");
47111

48-
let err = err.source().expect("inner error exists");
49-
assert_eq!(
50-
err.to_string(),
112+
let err_msg = DisplayErrorContext(&err).to_string();
113+
assert_str_contains!(
114+
err_msg,
51115
"minimum throughput was specified at 1 B/s, but throughput of 0 B/s was observed"
52116
);
53117
}
54118

55119
async fn start_faulty_upload_server() -> (impl Future<Output = ()>, SocketAddr) {
56-
use tokio::net::{TcpListener, TcpStream};
57-
use tokio::time::sleep;
58-
59120
let listener = TcpListener::bind("0.0.0.0:0")
60121
.await
61122
.expect("socket is free");
@@ -65,12 +126,7 @@ async fn start_faulty_upload_server() -> (impl Future<Output = ()>, SocketAddr)
65126
let mut buf = BytesMut::new();
66127
let mut time_to_stall = false;
67128

68-
loop {
69-
if time_to_stall {
70-
debug!("faulty server has read partial request, now getting stuck");
71-
break;
72-
}
73-
129+
while !time_to_stall {
74130
match socket.try_read_buf(&mut buf) {
75131
Ok(0) => {
76132
unreachable!(
@@ -79,12 +135,7 @@ async fn start_faulty_upload_server() -> (impl Future<Output = ()>, SocketAddr)
79135
}
80136
Ok(n) => {
81137
debug!("read {n} bytes from the socket");
82-
83-
// Check to see if we've received some headers
84138
if buf.len() >= 128 {
85-
let s = String::from_utf8_lossy(&buf);
86-
debug!("{s}");
87-
88139
time_to_stall = true;
89140
}
90141
}
@@ -98,6 +149,7 @@ async fn start_faulty_upload_server() -> (impl Future<Output = ()>, SocketAddr)
98149
}
99150
}
100151

152+
debug!("faulty server has read partial request, now getting stuck");
101153
loop {
102154
tokio::task::yield_now().await
103155
}
@@ -240,9 +292,6 @@ async fn test_stalled_stream_protection_for_downloads_is_enabled_by_default() {
240292
}
241293

242294
async fn start_faulty_download_server() -> (impl Future<Output = ()>, SocketAddr) {
243-
use tokio::net::{TcpListener, TcpStream};
244-
use tokio::time::sleep;
245-
246295
let listener = TcpListener::bind("0.0.0.0:0")
247296
.await
248297
.expect("socket is free");

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/StalledStreamProtectionConfigCustomization.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,12 @@ class StalledStreamProtectionOperationCustomization(
120120
is OperationSection.AdditionalInterceptors -> {
121121
val stalledStreamProtectionModule = RuntimeType.smithyRuntime(rc).resolve("client::stalled_stream_protection")
122122
section.registerInterceptor(rc, this) {
123-
// Currently, only response bodies are protected/supported because
124-
// we can't count on hyper to poll a request body on wake.
125123
rustTemplate(
126124
"""
127-
#{StalledStreamProtectionInterceptor}::new(#{Kind}::ResponseBody)
125+
#{StalledStreamProtectionInterceptor}::default()
128126
""",
129127
*preludeScope,
130128
"StalledStreamProtectionInterceptor" to stalledStreamProtectionModule.resolve("StalledStreamProtectionInterceptor"),
131-
"Kind" to stalledStreamProtectionModule.resolve("StalledStreamProtectionInterceptorKind"),
132129
)
133130
}
134131
}

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

0 commit comments

Comments
 (0)