use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct TransportControl<'a> {
pub cancel: &'a AtomicBool,
pub deadline: Option<Instant>,
}
impl<'a> TransportControl<'a> {
#[must_use]
pub fn with_cancel(mut self, cancel: &'a AtomicBool) -> Self {
self.cancel = cancel;
self
}
#[must_use]
pub fn with_deadline(mut self, deadline: Option<Instant>) -> Self {
self.deadline = deadline;
self
}
}
impl<'a> TransportControl<'a> {
pub fn new(cancel: &'a AtomicBool) -> Self {
Self {
cancel,
deadline: None,
}
}
pub(crate) fn check(self) -> io::Result<()> {
if self.cancel.load(Ordering::Relaxed) {
Err(io::Error::other(Interruption::Cancelled))
} else if self.deadline.is_some_and(|end| Instant::now() >= end) {
Err(io::Error::new(
io::ErrorKind::TimedOut,
Interruption::Deadline,
))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
pub(crate) enum Interruption {
#[error("transport cancelled")]
Cancelled,
#[error("transport deadline expired")]
Deadline,
}
pub(crate) fn interruption(error: &io::Error) -> Option<Interruption> {
error.get_ref()?.downcast_ref::<Interruption>().copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancellation_wins_when_deadline_also_expired() {
let cancel = AtomicBool::new(true);
let control = TransportControl {
cancel: &cancel,
deadline: Some(Instant::now()),
};
let error = control.check().unwrap_err();
assert!(matches!(
interruption(&error),
Some(Interruption::Cancelled)
));
assert_ne!(error.kind(), io::ErrorKind::Interrupted);
}
}