xmlstream: implement simple timeout logic

This allows to detect and handle dying streams without getting stuck
forever.

Timeouts are always wrong, though, so we put the burden of choosing the
right values (mostly) on the creator of a stream.
This commit is contained in:
Jonas Schäfer 2024-08-18 17:40:39 +02:00
commit 4cfe4f8429
16 changed files with 469 additions and 76 deletions

View file

@ -4,6 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::time::Duration;
use futures::{SinkExt, StreamExt};
use xmpp_parsers::stream_features::StreamFeatures;
@ -29,12 +31,18 @@ async fn test_initiate_accept_stream() {
to: Some("server".into()),
id: Some("client-id".into()),
},
Timeouts::tight(),
)
.await?;
Ok::<_, io::Error>(stream.take_header())
});
let responder = tokio::spawn(async move {
let stream = accept_stream(tokio::io::BufStream::new(rhs), "jabber:client").await?;
let stream = accept_stream(
tokio::io::BufStream::new(rhs),
"jabber:client",
Timeouts::tight(),
)
.await?;
assert_eq!(stream.header().from.unwrap(), "client");
assert_eq!(stream.header().to.unwrap(), "server");
assert_eq!(stream.header().id.unwrap(), "client-id");
@ -61,13 +69,19 @@ async fn test_exchange_stream_features() {
tokio::io::BufStream::new(lhs),
"jabber:client",
StreamHeader::default(),
Timeouts::tight(),
)
.await?;
let (features, _) = stream.recv_features::<Data>().await?;
Ok::<_, io::Error>(features)
});
let responder = tokio::spawn(async move {
let stream = accept_stream(tokio::io::BufStream::new(rhs), "jabber:client").await?;
let stream = accept_stream(
tokio::io::BufStream::new(rhs),
"jabber:client",
Timeouts::tight(),
)
.await?;
let stream = stream.send_header(StreamHeader::default()).await?;
stream
.send_features::<Data>(&StreamFeatures::default())
@ -88,6 +102,7 @@ async fn test_exchange_data() {
tokio::io::BufStream::new(lhs),
"jabber:client",
StreamHeader::default(),
Timeouts::tight(),
)
.await?;
let (_, mut stream) = stream.recv_features::<Data>().await?;
@ -104,7 +119,12 @@ async fn test_exchange_data() {
});
let responder = tokio::spawn(async move {
let stream = accept_stream(tokio::io::BufStream::new(rhs), "jabber:client").await?;
let stream = accept_stream(
tokio::io::BufStream::new(rhs),
"jabber:client",
Timeouts::tight(),
)
.await?;
let stream = stream.send_header(StreamHeader::default()).await?;
let mut stream = stream
.send_features::<Data>(&StreamFeatures::default())
@ -134,6 +154,7 @@ async fn test_clean_shutdown() {
tokio::io::BufStream::new(lhs),
"jabber:client",
StreamHeader::default(),
Timeouts::tight(),
)
.await?;
let (_, mut stream) = stream.recv_features::<Data>().await?;
@ -146,7 +167,12 @@ async fn test_clean_shutdown() {
});
let responder = tokio::spawn(async move {
let stream = accept_stream(tokio::io::BufStream::new(rhs), "jabber:client").await?;
let stream = accept_stream(
tokio::io::BufStream::new(rhs),
"jabber:client",
Timeouts::tight(),
)
.await?;
let stream = stream.send_header(StreamHeader::default()).await?;
let mut stream = stream
.send_features::<Data>(&StreamFeatures::default())
@ -172,6 +198,7 @@ async fn test_exchange_data_stream_reset_and_shutdown() {
tokio::io::BufStream::new(lhs),
"jabber:client",
StreamHeader::default(),
Timeouts::tight(),
)
.await?;
let (_, mut stream) = stream.recv_features::<Data>().await?;
@ -215,7 +242,12 @@ async fn test_exchange_data_stream_reset_and_shutdown() {
});
let responder = tokio::spawn(async move {
let stream = accept_stream(tokio::io::BufStream::new(rhs), "jabber:client").await?;
let stream = accept_stream(
tokio::io::BufStream::new(rhs),
"jabber:client",
Timeouts::tight(),
)
.await?;
let stream = stream.send_header(StreamHeader::default()).await?;
let mut stream = stream
.send_features::<Data>(&StreamFeatures::default())
@ -262,3 +294,104 @@ async fn test_exchange_data_stream_reset_and_shutdown() {
responder.await.unwrap().expect("responder failed");
initiator.await.unwrap().expect("initiator failed");
}
#[tokio::test(start_paused = true)]
async fn test_emits_soft_timeout_after_silence() {
let (lhs, rhs) = tokio::io::duplex(65536);
let client_timeouts = Timeouts {
read_timeout: Duration::new(300, 0),
response_timeout: Duration::new(15, 0),
};
// We do want to trigger only one set of timeouts, so we set the server
// timeouts much longer than the client timeouts
let server_timeouts = Timeouts {
read_timeout: Duration::new(900, 0),
response_timeout: Duration::new(15, 0),
};
let initiator = tokio::spawn(async move {
let stream = initiate_stream(
tokio::io::BufStream::new(lhs),
"jabber:client",
StreamHeader::default(),
client_timeouts,
)
.await?;
let (_, mut stream) = stream.recv_features::<Data>().await?;
stream
.send(&Data {
contents: "hello".to_owned(),
})
.await?;
match stream.next().await {
Some(Ok(Data { contents })) => assert_eq!(contents, "world!"),
other => panic!("unexpected stream message: {:?}", other),
}
// Here we prove that the stream doesn't see any data and also does
// not see the SoftTimeout too early.
// (Well, not exactly a proof: We only check until half of the read
// timeout, because that was easy to write and I deem it good enough.)
match tokio::time::timeout(client_timeouts.read_timeout / 2, stream.next()).await {
Err(_) => (),
Ok(ev) => panic!("early stream message (before soft timeout): {:?}", ev),
};
// Now the next thing that happens is the soft timeout ...
match stream.next().await {
Some(Err(ReadError::SoftTimeout)) => (),
other => panic!("unexpected stream message: {:?}", other),
}
// Another check that the there is some time between soft and hard
// timeout.
match tokio::time::timeout(client_timeouts.response_timeout / 3, stream.next()).await {
Err(_) => (),
Ok(ev) => {
panic!("early stream message (before hard timeout): {:?}", ev);
}
};
// ... and thereafter the hard timeout in form of an I/O error.
match stream.next().await {
Some(Err(ReadError::HardError(e))) if e.kind() == io::ErrorKind::TimedOut => (),
other => panic!("unexpected stream message: {:?}", other),
}
Ok::<_, io::Error>(())
});
let responder = tokio::spawn(async move {
let stream = accept_stream(
tokio::io::BufStream::new(rhs),
"jabber:client",
server_timeouts,
)
.await?;
let stream = stream.send_header(StreamHeader::default()).await?;
let mut stream = stream
.send_features::<Data>(&StreamFeatures::default())
.await?;
stream
.send(&Data {
contents: "world!".to_owned(),
})
.await?;
match stream.next().await {
Some(Ok(Data { contents })) => assert_eq!(contents, "hello"),
other => panic!("unexpected stream message: {:?}", other),
}
match stream.next().await {
Some(Err(ReadError::HardError(e))) if e.kind() == io::ErrorKind::InvalidData => {
match e.downcast::<rxml::Error>() {
// the initiator closes the stream by dropping it once the
// timeout trips, so we get a hard eof here.
Ok(rxml::Error::InvalidEof(_)) => (),
other => panic!("unexpected error: {:?}", other),
}
}
other => panic!("unexpected stream message: {:?}", other),
}
Ok::<_, io::Error>(())
});
responder.await.unwrap().expect("responder failed");
initiator.await.unwrap().expect("initiator failed");
}