use crate::StatusCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EndedBy {
Local,
NotLocal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StreamState {
InSync,
Lost,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EndReason {
EndOfStream,
UncleanEndOfStream,
Unreadable,
Retired,
Interrupted,
DeadlineMidFrame,
Frame(StatusCode),
Dispatch(StatusCode),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[must_use = "the serve loop's end says whether the stream was lost; call into_result() or is_clean()"]
pub struct SessionEnd {
pub by: EndedBy,
pub stream: StreamState,
pub reason: EndReason,
}
impl SessionEnd {
pub(crate) fn new(reason: EndReason, ended_locally: bool, deadline_armed: bool) -> Self {
use EndReason::*;
use StreamState::*;
let idle_eviction = deadline_armed && matches!(reason, Frame(StatusCode::TimedOut));
let our_cut = deadline_armed && matches!(reason, DeadlineMidFrame);
let decided = ended_locally || idle_eviction || our_cut || matches!(reason, Interrupted);
let by = if decided {
EndedBy::Local
} else {
EndedBy::NotLocal
};
let stream = match reason {
EndOfStream | Interrupted => InSync,
Frame(StatusCode::TimedOut) if idle_eviction => InSync,
UncleanEndOfStream | Unreadable | DeadlineMidFrame | Frame(_) => Lost,
Retired | Dispatch(_) => {
if ended_locally {
InSync
} else {
Lost
}
}
};
SessionEnd { by, stream, reason }
}
pub fn into_result(self) -> crate::Result<()> {
match self.stream {
StreamState::InSync => Ok(()),
StreamState::Lost => Err(StatusCode::DeadObject),
}
}
pub fn is_clean(&self) -> bool {
matches!(self.stream, StreamState::InSync)
}
pub(crate) fn log(&self, what: &str) {
match self.stream {
StreamState::Lost => log::warn!("{what}: {self}"),
StreamState::InSync => log::debug!("{what}: {self}"),
}
}
}
impl std::fmt::Display for SessionEnd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let by = match self.by {
EndedBy::Local => "ended by this end",
EndedBy::NotLocal => "ended by the peer or the stream",
};
let stream = match self.stream {
StreamState::InSync => "stream intact",
StreamState::Lost => "stream lost",
};
write!(f, "{by}, {stream} ({:?})", self.reason)
}
}
pub(crate) enum ServeStep {
Continue,
Ended(EndReason),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn projection_reads_the_stream_axis_only() {
use EndReason::*;
use EndedBy::{Local, NotLocal};
use StatusCode::{BadType, DeadObject, NotEnoughData, TimedOut};
use StreamState::{InSync, Lost};
let cases: &[(EndReason, bool, bool, EndedBy, StreamState, bool)] = &[
(EndOfStream, false, false, NotLocal, InSync, true),
(EndOfStream, true, false, Local, InSync, true),
(UncleanEndOfStream, false, false, NotLocal, Lost, false),
(UncleanEndOfStream, true, false, Local, Lost, false),
(Unreadable, false, false, NotLocal, Lost, false),
(Unreadable, true, false, Local, Lost, false),
(Retired, false, false, NotLocal, Lost, false),
(Retired, true, false, Local, InSync, true),
(Interrupted, true, false, Local, InSync, true),
(Interrupted, false, false, Local, InSync, true),
(DeadlineMidFrame, false, true, Local, Lost, false),
(DeadlineMidFrame, false, false, NotLocal, Lost, false),
(DeadlineMidFrame, true, false, Local, Lost, false),
(Frame(TimedOut), false, true, Local, InSync, true),
(Frame(TimedOut), false, false, NotLocal, Lost, false),
(Frame(TimedOut), true, false, Local, Lost, false),
(Frame(NotEnoughData), false, false, NotLocal, Lost, false),
(Frame(BadType), true, false, Local, Lost, false),
(Dispatch(DeadObject), false, false, NotLocal, Lost, false),
(Dispatch(DeadObject), true, false, Local, InSync, true),
];
for &(reason, local, armed, by, stream, ok) in cases {
let end = SessionEnd::new(reason, local, armed);
assert_eq!(end.by, by, "{reason:?} local={local} armed={armed}");
assert_eq!(end.stream, stream, "{reason:?} local={local} armed={armed}");
assert_eq!(end.is_clean(), ok, "{reason:?} local={local} armed={armed}");
let expected = if ok {
Ok(())
} else {
Err(StatusCode::DeadObject)
};
assert_eq!(
end.into_result(),
expected,
"{reason:?} local={local} armed={armed}"
);
}
}
}