Skip to content

Commit 2f3c7ea

Browse files
committed
test: add regression test for stream close-frame ordering
Exercises the fix in the previous commit. A server-streaming handler sends one payload and returns (DATA frame immediately followed by REMOTE_CLOSED). The client calls it 1000 times on a multi_thread runtime and asserts that every iteration receives the payload before EOF. Without the handle_msg fix this fails with ~1% of iterations silently dropping the payload. Signed-off-by: Shiv Bhosale <shvbsle@amazon.com>
1 parent c908838 commit 2f3c7ea

3 files changed

Lines changed: 174 additions & 1 deletion

File tree

example/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ simple-logging = "2.0.2"
2121
nix = "0.23.0"
2222
ttrpc = { path = "../", features = ["async"] }
2323
ctrlc = { version = "3.0", features = ["termination"] }
24-
tokio = { version = "1.0.1", features = ["signal", "time"] }
24+
tokio = { version = "1.0.1", features = ["signal", "time", "rt-multi-thread", "macros"] }
2525
async-trait = "0.1.42"
2626
rand = "0.8.5"
2727
clap = { version = "4.5.40", features = ["derive"] }
@@ -50,5 +50,9 @@ path = "./async-stream-server.rs"
5050
name = "async-stream-client"
5151
path = "./async-stream-client.rs"
5252

53+
[[example]]
54+
name = "async-stream-close-order"
55+
path = "./async-stream-close-order.rs"
56+
5357
[build-dependencies]
5458
ttrpc-codegen = { path = "../ttrpc-codegen"}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Regression test for per-stream frame ordering on the async client.
2+
//
3+
// A server-streaming handler sends one payload and returns, producing a DATA
4+
// frame immediately followed by a REMOTE_CLOSED frame on the wire. The client
5+
// calls this 1000 times on a multi-threaded runtime and asserts that every
6+
// iteration receives the payload before EOF.
7+
8+
mod protocols;
9+
10+
use std::sync::Arc;
11+
12+
use async_trait::async_trait;
13+
use protocols::asynchronous::{empty, streaming, streaming_ttrpc};
14+
use ttrpc::asynchronous::{Client, Server};
15+
16+
const SOCK: &str = "unix:///tmp/ttrpc-test-close-order";
17+
const ITERATIONS: usize = 1000;
18+
19+
struct Svc;
20+
21+
#[async_trait]
22+
impl streaming_ttrpc::Streaming for Svc {
23+
async fn echo(
24+
&self,
25+
_ctx: &::ttrpc::r#async::TtrpcContext,
26+
_req: streaming::EchoPayload,
27+
) -> ::ttrpc::Result<streaming::EchoPayload> {
28+
unimplemented!()
29+
}
30+
31+
async fn echo_stream(
32+
&self,
33+
_ctx: &::ttrpc::r#async::TtrpcContext,
34+
_s: ::ttrpc::r#async::ServerStream<streaming::EchoPayload, streaming::EchoPayload>,
35+
) -> ::ttrpc::Result<()> {
36+
unimplemented!()
37+
}
38+
39+
async fn sum_stream(
40+
&self,
41+
_ctx: &::ttrpc::r#async::TtrpcContext,
42+
_s: ::ttrpc::r#async::ServerStreamReceiver<streaming::Part>,
43+
) -> ::ttrpc::Result<streaming::Sum> {
44+
unimplemented!()
45+
}
46+
47+
async fn divide_stream(
48+
&self,
49+
_ctx: &::ttrpc::r#async::TtrpcContext,
50+
_sum: streaming::Sum,
51+
_s: ::ttrpc::r#async::ServerStreamSender<streaming::Part>,
52+
) -> ::ttrpc::Result<()> {
53+
unimplemented!()
54+
}
55+
56+
async fn echo_null(
57+
&self,
58+
_ctx: &::ttrpc::r#async::TtrpcContext,
59+
_s: ::ttrpc::r#async::ServerStreamReceiver<streaming::EchoPayload>,
60+
) -> ::ttrpc::Result<empty::Empty> {
61+
unimplemented!()
62+
}
63+
64+
async fn echo_null_stream(
65+
&self,
66+
_ctx: &::ttrpc::r#async::TtrpcContext,
67+
_s: ::ttrpc::r#async::ServerStream<empty::Empty, streaming::EchoPayload>,
68+
) -> ::ttrpc::Result<()> {
69+
unimplemented!()
70+
}
71+
72+
async fn echo_default_value(
73+
&self,
74+
_ctx: &::ttrpc::r#async::TtrpcContext,
75+
_req: streaming::EchoPayload,
76+
s: ::ttrpc::r#async::ServerStreamSender<streaming::EchoPayload>,
77+
) -> ::ttrpc::Result<()> {
78+
s.send(&streaming::EchoPayload {
79+
seq: 1,
80+
msg: "hello".into(),
81+
..Default::default()
82+
})
83+
.await
84+
.unwrap();
85+
Ok(())
86+
}
87+
88+
async fn server_send_stream(
89+
&self,
90+
_ctx: &::ttrpc::r#async::TtrpcContext,
91+
_req: empty::Empty,
92+
_s: ::ttrpc::r#async::ServerStreamSender<streaming::EchoPayload>,
93+
) -> ::ttrpc::Result<()> {
94+
unimplemented!()
95+
}
96+
}
97+
98+
#[tokio::main(flavor = "multi_thread")]
99+
async fn main() {
100+
let path = SOCK.strip_prefix("unix://").unwrap();
101+
let _ = std::fs::remove_file(path);
102+
103+
let service = streaming_ttrpc::create_streaming(Arc::new(Svc {}));
104+
let mut server = Server::new().bind(SOCK).unwrap().register_service(service);
105+
server.start().await.unwrap();
106+
107+
let c = Client::connect(SOCK).await.unwrap();
108+
let sc = streaming_ttrpc::StreamingClient::new(c);
109+
110+
let mut got_payload = 0usize;
111+
let mut eof_without_payload = 0usize;
112+
113+
for _ in 0..ITERATIONS {
114+
let ctx = ttrpc::context::with_timeout(5_000_000_000);
115+
let mut stream = sc
116+
.echo_default_value(ctx, &streaming::EchoPayload::default())
117+
.await
118+
.expect("failed to open stream");
119+
120+
match stream.recv().await {
121+
Ok(Some(_)) => got_payload += 1,
122+
Ok(None) => eof_without_payload += 1,
123+
Err(e) => panic!("unexpected error from recv: {:?}", e),
124+
}
125+
}
126+
127+
server.shutdown().await.unwrap();
128+
129+
eprintln!(
130+
"iterations={ITERATIONS} got_payload={got_payload} eof_without_payload={eof_without_payload}"
131+
);
132+
assert_eq!(
133+
eof_without_payload, 0,
134+
"server sent a payload on every iteration but the client saw EOF \
135+
{eof_without_payload}/{ITERATIONS} times (data frames dropped)"
136+
);
137+
}

tests/run-examples.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,35 @@ fn run_examples() -> Result<(), Box<dyn std::error::Error>> {
109109

110110
Ok(())
111111
}
112+
113+
#[test]
114+
#[cfg(unix)]
115+
fn stream_close_order() -> Result<(), Box<dyn std::error::Error>> {
116+
// Self-contained test: server + client in one process, multi-threaded
117+
// runtime. Verifies that the final DATA frame of a server-streaming RPC
118+
// is not dropped due to a race with the subsequent REMOTE_CLOSED frame.
119+
let mut cmd = do_run_example("async-stream-close-order", &[]);
120+
let mut child = cmd.spawn().unwrap();
121+
122+
let timeout = Duration::from_secs(120);
123+
let start = std::time::Instant::now();
124+
loop {
125+
if start.elapsed() > timeout {
126+
child.kill().unwrap_or(());
127+
panic!("async-stream-close-order timed out");
128+
}
129+
match child.try_wait() {
130+
Ok(Some(status)) => {
131+
wait_with_output("async-stream-close-order", child);
132+
assert!(
133+
status.success(),
134+
"async-stream-close-order failed (data frames dropped)"
135+
);
136+
break;
137+
}
138+
Ok(None) => continue,
139+
Err(e) => panic!("Error waiting for async-stream-close-order: {:?}", e),
140+
}
141+
}
142+
Ok(())
143+
}

0 commit comments

Comments
 (0)