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:
parent
960fd782bd
commit
4cfe4f8429
16 changed files with 469 additions and 76 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue