use super::{Depth, DepthOf, FeedError};
use crate::error::ErrorClass;
use std::time::{Duration, Instant};
pub const END_CHECK_INTERVAL: Duration = Duration::from_secs(20);
const READ_EVERY: Duration = Duration::from_secs(15);
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MarkerRead {
Present(String),
Absent,
Failed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EndAction {
Wait(Option<Instant>),
ReadMarker,
ReadDepth(DepthOf),
Receive,
Ended(EndOutcome),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EndOutcome {
Completed,
Stalled(Depth),
Failed(FeedError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Read {
Marker,
Depth(DepthOf),
Receive,
}
#[derive(Debug)]
enum Phase {
Opening {
due: Instant,
},
Window {
run: String,
start: Option<Instant>,
last: Instant,
due: Instant,
},
Closing {
run: String,
},
Receiving,
DeadLetters {
due: Instant,
},
Ended(EndOutcome),
}
#[derive(Debug)]
pub struct EndCheck {
window: Option<Duration>,
phase: Phase,
pending: Option<(Read, Instant)>,
last_request: Option<Instant>,
announced: Option<String>,
}
impl EndCheck {
#[must_use]
pub fn windowed(window: Duration, now: Instant) -> EndCheck {
EndCheck {
window: Some(window),
phase: Phase::Opening { due: now },
pending: None,
last_request: None,
announced: None,
}
}
#[must_use]
pub fn exact(now: Instant) -> EndCheck {
EndCheck {
window: None,
phase: Phase::Opening { due: now },
pending: None,
last_request: None,
announced: None,
}
}
pub fn next(&mut self, now: Instant) -> EndAction {
if self.pending.is_some() {
return EndAction::Wait(None);
}
if let Phase::Window { last, .. } = self.phase
&& now > last + END_CHECK_INTERVAL
{
tracing::info!(
gap = ?now - last,
"the feed went unread longer than the read interval; restarting the end check"
);
self.reopen(now);
}
let read = match &self.phase {
Phase::Ended(outcome) => return EndAction::Ended(outcome.clone()),
Phase::Opening { due } | Phase::Window { due, .. } | Phase::DeadLetters { due }
if now < *due =>
{
return EndAction::Wait(Some(*due));
}
Phase::Opening { .. } if self.window.is_none() => Read::Depth(DepthOf::Feed),
Phase::Opening { .. } | Phase::Closing { .. } => Read::Marker,
Phase::Window { .. } => Read::Depth(DepthOf::Feed),
Phase::Receiving => Read::Receive,
Phase::DeadLetters { .. } => Read::Depth(DepthOf::DeadLetters),
};
self.pending = Some((read, now));
if matches!(read, Read::Marker | Read::Depth(DepthOf::Feed)) {
self.last_request = Some(now);
}
match read {
Read::Marker => EndAction::ReadMarker,
Read::Depth(of) => EndAction::ReadDepth(of),
Read::Receive => EndAction::Receive,
}
}
pub fn marker_read(&mut self, now: Instant, read: MarkerRead) {
let Some(requested) = self.take_pending(Read::Marker) else {
return;
};
if now > requested + END_CHECK_INTERVAL {
tracing::info!(
late = ?now - requested,
"an end marker read answered after the read interval; restarting the end check"
);
self.reopen(now);
return;
}
let window = self.window.unwrap_or_default();
let closing = match &self.phase {
Phase::Closing { run } => Some(run.clone()),
_ => None,
};
match (read, closing) {
(MarkerRead::Present(seen), Some(run)) if seen == run => {
self.phase = Phase::Receiving;
}
(MarkerRead::Present(seen), closing) => {
if closing.is_some() {
tracing::info!(
run = %seen,
"a newer lister run's end marker replaced the window's; \
opening a new settling window"
);
} else if self.announced.as_deref() != Some(seen.as_str()) {
tracing::info!(
run = %seen,
window = ?window,
"end marker found; the job ends once the feed stays empty over \
the settling window"
);
}
self.announced = Some(seen.clone());
self.phase = Phase::Window {
run: seen,
start: None,
last: requested,
due: now,
};
}
(MarkerRead::Absent, closing) => {
self.announced = None;
if closing.is_some() {
tracing::info!(
"the end marker is gone at the settling window's end; waiting for a \
lister run's marker"
);
}
self.phase = Phase::Opening {
due: requested + READ_EVERY,
};
}
(MarkerRead::Failed, _) => {
tracing::warn!("reading the end marker failed; reading it again");
self.phase = Phase::Opening {
due: requested + READ_EVERY,
};
}
}
}
pub fn depth_read(&mut self, now: Instant, read: Result<Depth, FeedError>) {
let Some((Read::Depth(of), requested)) = self.pending else {
return;
};
self.pending = None;
let due = requested + READ_EVERY;
match (of, read) {
(_, Err(e)) if e.class() == ErrorClass::Fatal => {
self.phase = Phase::Ended(EndOutcome::Failed(e));
}
(DepthOf::DeadLetters, Ok(depth)) if depth.is_empty() => {
self.phase = Phase::Ended(EndOutcome::Completed);
}
(DepthOf::DeadLetters, Ok(depth)) => {
self.phase = Phase::Ended(EndOutcome::Stalled(depth));
}
(DepthOf::DeadLetters, Err(e)) => {
tracing::warn!(error = %e, "reading the dead-letter destination failed; reading it again");
self.phase = Phase::DeadLetters { due };
}
(DepthOf::Feed, Err(e)) => {
tracing::warn!(error = %e, "reading the feed's depth failed; restarting the end check");
self.reopen(due);
}
(DepthOf::Feed, Ok(depth)) if !depth.is_empty() => {
tracing::debug!(%depth, "the feed is not empty; restarting the end check");
self.reopen(due);
}
(DepthOf::Feed, Ok(_))
if self.window.is_some() && now > requested + END_CHECK_INTERVAL =>
{
tracing::info!(
late = ?now - requested,
"a depth read answered after the read interval; restarting the end check"
);
self.reopen(now);
}
(DepthOf::Feed, Ok(_)) => self.advance(requested),
}
}
pub fn received(&mut self, now: Instant, took: bool) {
if self.take_pending(Read::Receive).is_none() {
return;
}
if took {
self.restart(now);
} else {
self.phase = Phase::DeadLetters { due: now };
}
}
pub fn restart(&mut self, now: Instant) {
if matches!(self.phase, Phase::Ended(_)) {
return;
}
if !matches!(self.phase, Phase::Opening { .. }) {
tracing::info!("the reader took work; restarting the end check");
}
self.pending = None;
let due = match (self.window, self.last_request) {
(Some(_), Some(last)) => (last + READ_EVERY).max(now),
_ => now,
};
self.phase = Phase::Opening { due };
}
#[must_use]
pub fn outcome(&self) -> Option<&EndOutcome> {
match &self.phase {
Phase::Ended(outcome) => Some(outcome),
_ => None,
}
}
fn take_pending(&mut self, read: Read) -> Option<Instant> {
match self.pending {
Some((pending, requested)) if pending == read => {
self.pending = None;
Some(requested)
}
_ => None,
}
}
fn reopen(&mut self, due: Instant) {
self.phase = Phase::Opening { due };
}
fn advance(&mut self, requested: Instant) {
match (&self.phase, self.window) {
(Phase::Opening { .. }, None) => self.phase = Phase::Receiving,
(Phase::Window { run, start, .. }, Some(window)) => {
let start = start.unwrap_or(requested);
let closes = start
.checked_add(window)
.is_some_and(|end| requested >= end);
self.phase = if closes {
Phase::Closing { run: run.clone() }
} else {
Phase::Window {
run: run.clone(),
start: Some(start),
last: requested,
due: requested + READ_EVERY,
}
};
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::feed::FeedErrorKind;
const WINDOW: Duration = Duration::from_secs(60);
fn at(start: Instant, secs: u64) -> Instant {
start + Duration::from_secs(secs)
}
fn empty() -> Result<Depth, FeedError> {
Ok(Depth::new("queue", true))
}
fn visible(n: u64) -> Result<Depth, FeedError> {
let mut depth = Depth::new("queue", true);
depth.visible = n;
Ok(depth)
}
fn failed() -> Result<Depth, FeedError> {
Err(FeedError::new(FeedErrorKind::Retryable, "throttled"))
}
fn present(run: &str) -> MarkerRead {
MarkerRead::Present(run.into())
}
fn opened(start: Instant, run: &str) -> EndCheck {
let mut check = EndCheck::windowed(WINDOW, start);
assert_eq!(check.next(start), EndAction::ReadMarker);
check.marker_read(start, present(run));
check
}
fn empty_reads(check: &mut EndCheck, start: Instant, from: u64, to: u64) -> usize {
let mut reads = 0;
for secs in (from..=to).step_by(15) {
let now = at(start, secs);
assert_eq!(
check.next(now),
EndAction::ReadDepth(DepthOf::Feed),
"read at {secs}s"
);
check.depth_read(now, empty());
reads += 1;
}
reads
}
fn finish(check: &mut EndCheck, now: Instant, dead: Result<Depth, FeedError>) -> EndAction {
assert_eq!(check.next(now), EndAction::Receive);
check.received(now, false);
assert_eq!(check.next(now), EndAction::ReadDepth(DepthOf::DeadLetters));
check.depth_read(now, dead);
check.next(now)
}
#[test]
fn a_window_ends_one_window_after_its_opening_marker_read() {
let start = Instant::now();
let mut check = opened(start, "a");
assert_eq!(empty_reads(&mut check, start, 0, 45), 4);
assert_eq!(
check.next(at(start, 59)),
EndAction::Wait(Some(at(start, 60)))
);
empty_reads(&mut check, start, 60, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 60), present("a"));
assert_eq!(
finish(&mut check, at(start, 60), empty()),
EndAction::Ended(EndOutcome::Completed)
);
}
#[test]
fn a_nonzero_read_restarts_the_window() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 15);
assert_eq!(
check.next(at(start, 30)),
EndAction::ReadDepth(DepthOf::Feed)
);
check.depth_read(at(start, 30), visible(1));
assert_eq!(
check.next(at(start, 44)),
EndAction::Wait(Some(at(start, 45)))
);
assert_eq!(check.next(at(start, 45)), EndAction::ReadMarker);
check.marker_read(at(start, 45), present("a"));
empty_reads(&mut check, start, 45, 90);
assert_eq!(
check.next(at(start, 105)),
EndAction::ReadDepth(DepthOf::Feed),
"the window counts from the marker read at 45 s"
);
check.depth_read(at(start, 105), empty());
assert_eq!(check.next(at(start, 105)), EndAction::ReadMarker);
}
#[test]
fn a_failed_read_restarts_the_window() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 30);
assert_eq!(
check.next(at(start, 45)),
EndAction::ReadDepth(DepthOf::Feed)
);
check.depth_read(at(start, 45), failed());
assert_eq!(
check.next(at(start, 59)),
EndAction::Wait(Some(at(start, 60))),
"a failed read is followed 15 s after its request"
);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
assert!(check.outcome().is_none());
}
#[test]
fn a_read_answered_late_restarts_the_window() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 15);
assert_eq!(
check.next(at(start, 30)),
EndAction::ReadDepth(DepthOf::Feed)
);
check.depth_read(at(start, 51), empty());
assert_eq!(check.next(at(start, 51)), EndAction::ReadMarker);
let mut check = opened(start, "a");
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(at(start, 20), empty());
assert_eq!(
check.next(at(start, 20)),
EndAction::ReadDepth(DepthOf::Feed)
);
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 45);
assert_eq!(
check.next(at(start, 60)),
EndAction::ReadDepth(DepthOf::Feed)
);
check.depth_read(at(start, 81), empty());
assert_eq!(check.next(at(start, 81)), EndAction::ReadMarker);
check.marker_read(at(start, 81), present("a"));
assert_eq!(
check.next(at(start, 81)),
EndAction::ReadDepth(DepthOf::Feed)
);
}
#[test]
fn a_read_taken_after_a_gap_restarts_the_window() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 15);
assert_eq!(check.next(at(start, 36)), EndAction::ReadMarker);
assert!(check.outcome().is_none());
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 15);
assert_eq!(
check.next(at(start, 35)),
EndAction::ReadDepth(DepthOf::Feed),
"20 s after the last read is within the bound"
);
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 0);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
}
#[test]
fn a_closing_marker_of_another_run_opens_a_new_window() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 60), present("b"));
empty_reads(&mut check, start, 60, 120);
assert_eq!(check.next(at(start, 120)), EndAction::ReadMarker);
check.marker_read(at(start, 120), present("b"));
assert_eq!(check.next(at(start, 120)), EndAction::Receive);
}
#[test]
fn an_absent_closing_marker_does_not_end() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 60), MarkerRead::Absent);
assert_eq!(
check.next(at(start, 60)),
EndAction::Wait(Some(at(start, 75))),
"an absent marker is read again 15 s later"
);
assert_eq!(check.next(at(start, 75)), EndAction::ReadMarker);
check.marker_read(at(start, 75), MarkerRead::Absent);
assert!(check.outcome().is_none());
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 60), MarkerRead::Failed);
assert_eq!(
check.next(at(start, 60)),
EndAction::Wait(Some(at(start, 75))),
"a failed marker read is retried 15 s later"
);
assert_eq!(check.next(at(start, 75)), EndAction::ReadMarker);
}
#[test]
fn a_marker_answer_noticed_late_keeps_the_schedule() {
let start = Instant::now();
let mut check = EndCheck::windowed(WINDOW, start);
assert_eq!(check.next(start), EndAction::ReadMarker);
check.marker_read(at(start, 10), MarkerRead::Absent);
assert_eq!(
check.next(at(start, 10)),
EndAction::Wait(Some(at(start, 15)))
);
assert_eq!(check.next(at(start, 15)), EndAction::ReadMarker);
check.marker_read(at(start, 25), present("a"));
empty_reads(&mut check, start, 30, 75);
assert_eq!(
check.next(at(start, 75)),
EndAction::Wait(Some(at(start, 90))),
"the read at 75 s is short of a window after the first read"
);
empty_reads(&mut check, start, 90, 90);
assert_eq!(check.next(at(start, 90)), EndAction::ReadMarker);
}
#[test]
fn a_window_spans_its_reads_from_the_first() {
let start = Instant::now();
let mut check = EndCheck::windowed(END_CHECK_INTERVAL, start);
assert_eq!(check.next(start), EndAction::ReadMarker);
check.marker_read(at(start, 20), present("a"));
empty_reads(&mut check, start, 20, 35);
assert_eq!(
check.next(at(start, 35)),
EndAction::Wait(Some(at(start, 50))),
"15 s of reads do not close a 20 s window"
);
empty_reads(&mut check, start, 50, 50);
assert_eq!(check.next(at(start, 50)), EndAction::ReadMarker);
}
#[test]
fn an_answer_nothing_asked_for_is_ignored() {
let start = Instant::now();
let mut check = opened(start, "a");
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.marker_read(start, present("b"));
check.received(start, true);
assert_eq!(check.next(start), EndAction::Wait(None));
check.depth_read(start, empty());
assert_eq!(
check.next(start),
EndAction::Wait(Some(at(start, 15))),
"the window on run a carries on"
);
check.depth_read(start, visible(1));
assert_eq!(
check.next(at(start, 15)),
EndAction::ReadDepth(DepthOf::Feed)
);
}
fn logs(f: impl FnOnce()) -> String {
#[derive(Clone, Default)]
struct Buf(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for Buf {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let buf = Buf::default();
let writer = buf.clone();
let subscriber = tracing_subscriber::fmt()
.with_writer(move || writer.clone())
.with_ansi(false)
.finish();
tracing::subscriber::with_default(subscriber, f);
let bytes = buf.0.lock().unwrap().clone();
String::from_utf8(bytes).unwrap()
}
#[test]
fn the_check_logs_a_window_opening_and_its_restart() {
let start = Instant::now();
let mut check = EndCheck::windowed(WINDOW, start);
let quiet = logs(|| check.restart(start));
assert!(quiet.is_empty(), "{quiet}");
assert_eq!(check.next(start), EndAction::ReadMarker);
let opened = logs(|| check.marker_read(start, present("run-a")));
assert!(
opened.contains("INFO")
&& opened.contains("end marker found")
&& opened.contains("run-a"),
"{opened}"
);
let restarted = logs(|| check.restart(start));
assert!(
restarted.contains("INFO") && restarted.contains("restarting the end check"),
"{restarted}"
);
assert_eq!(check.next(at(start, 15)), EndAction::ReadMarker);
let again = logs(|| check.marker_read(at(start, 15), present("run-a")));
assert!(again.is_empty(), "{again}");
assert_eq!(
check.next(at(start, 15)),
EndAction::ReadDepth(DepthOf::Feed)
);
let busy = logs(|| check.depth_read(at(start, 15), visible(1)));
assert!(!busy.contains("INFO") && !busy.contains("WARN"), "{busy}");
assert_eq!(check.next(at(start, 30)), EndAction::ReadMarker);
let absent = logs(|| check.marker_read(at(start, 30), MarkerRead::Absent));
assert!(!absent.contains("WARN"), "{absent}");
assert_eq!(check.next(at(start, 45)), EndAction::ReadMarker);
let failed = logs(|| check.marker_read(at(start, 45), MarkerRead::Failed));
assert!(
failed.contains("WARN") && failed.contains("reading the end marker failed"),
"{failed}"
);
}
#[test]
fn an_exact_check_logs_no_settling_window() {
let start = Instant::now();
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
let failed = logs(|| check.depth_read(start, failed()));
assert!(failed.contains("restarting the end check"), "{failed}");
let shown = failed + &logs(|| check.restart(start));
assert!(!shown.contains("settling window"), "{shown}");
}
#[test]
fn no_window_opens_before_a_marker_is_found() {
let start = Instant::now();
let mut check = EndCheck::windowed(WINDOW, start);
for secs in [0, 15, 30] {
assert_eq!(check.next(at(start, secs)), EndAction::ReadMarker);
check.marker_read(at(start, secs), MarkerRead::Absent);
}
assert_eq!(check.next(at(start, 45)), EndAction::ReadMarker);
check.marker_read(at(start, 45), present("a"));
assert_eq!(
check.next(at(start, 45)),
EndAction::ReadDepth(DepthOf::Feed)
);
}
#[test]
fn the_closing_marker_is_read_after_the_last_depth_read() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 45);
assert_eq!(
check.next(at(start, 60)),
EndAction::ReadDepth(DepthOf::Feed)
);
assert_eq!(check.next(at(start, 61)), EndAction::Wait(None));
check.depth_read(at(start, 61), empty());
assert_eq!(check.next(at(start, 61)), EndAction::ReadMarker);
}
#[test]
fn a_receive_that_takes_a_unit_restarts_the_window() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 60), present("a"));
assert_eq!(check.next(at(start, 62)), EndAction::Receive);
check.received(at(start, 62), true);
assert_eq!(
check.next(at(start, 62)),
EndAction::Wait(Some(at(start, 75))),
"15 s from the closing marker read, the last read of the marker or feed"
);
assert_eq!(check.next(at(start, 75)), EndAction::ReadMarker);
}
#[test]
fn a_restart_ignores_the_outstanding_read() {
let start = Instant::now();
let mut check = opened(start, "a");
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.restart(at(start, 5));
check.depth_read(at(start, 5), empty());
assert_eq!(
check.next(at(start, 5)),
EndAction::Wait(Some(at(start, 15)))
);
assert_eq!(check.next(at(start, 15)), EndAction::ReadMarker);
}
#[test]
fn a_failed_dead_letter_read_neither_drains_nor_stalls() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 60), present("a"));
assert_eq!(
finish(&mut check, at(start, 60), failed()),
EndAction::Wait(Some(at(start, 75)))
);
assert!(check.outcome().is_none());
assert_eq!(
check.next(at(start, 75)),
EndAction::ReadDepth(DepthOf::DeadLetters)
);
check.depth_read(at(start, 75), empty());
assert_eq!(check.outcome(), Some(&EndOutcome::Completed));
}
#[test]
fn one_dead_letter_stalls_and_none_completes() {
let start = Instant::now();
for (field, set) in [
("visible", (|d: &mut Depth| d.visible = 1) as fn(&mut Depth)),
("delayed", |d: &mut Depth| d.delayed = 1),
("in_flight", |d: &mut Depth| d.in_flight = 1),
] {
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, empty());
let mut dead = Depth::new("dead letters", true);
set(&mut dead);
assert_eq!(
finish(&mut check, start, Ok(dead.clone())),
EndAction::Ended(EndOutcome::Stalled(dead)),
"{field}"
);
}
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, empty());
assert_eq!(
finish(&mut check, start, empty()),
EndAction::Ended(EndOutcome::Completed)
);
}
#[test]
fn an_exact_feed_ends_on_its_first_empty_read() {
let start = Instant::now();
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, visible(1));
assert!(check.outcome().is_none());
check.restart(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, empty());
assert_eq!(
finish(&mut check, start, empty()),
EndAction::Ended(EndOutcome::Completed)
);
}
#[test]
fn a_fatal_depth_read_ends_the_check_failed() {
let start = Instant::now();
let fatal = FeedError::new(FeedErrorKind::Fatal, "access denied");
let mut check = opened(start, "a");
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, Err(fatal.clone()));
assert_eq!(
check.next(start),
EndAction::Ended(EndOutcome::Failed(fatal.clone()))
);
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, empty());
assert_eq!(check.next(start), EndAction::Receive);
check.received(start, false);
assert_eq!(
check.next(start),
EndAction::ReadDepth(DepthOf::DeadLetters)
);
check.depth_read(start, Err(fatal.clone()));
assert_eq!(check.outcome(), Some(&EndOutcome::Failed(fatal)));
let mut check = opened(start, "a");
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, failed());
assert!(check.outcome().is_none());
}
#[test]
fn a_marker_read_answered_late_restarts_the_check() {
let start = Instant::now();
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 81), present("a"));
assert_eq!(check.next(at(start, 81)), EndAction::ReadMarker);
let mut check = opened(start, "a");
empty_reads(&mut check, start, 0, 60);
assert_eq!(check.next(at(start, 60)), EndAction::ReadMarker);
check.marker_read(at(start, 80), present("a"));
assert_eq!(check.next(at(start, 80)), EndAction::Receive);
}
#[test]
fn an_exact_check_takes_a_read_answered_late() {
let start = Instant::now();
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(at(start, 100), empty());
assert_eq!(check.next(at(start, 100)), EndAction::Receive);
}
#[test]
fn a_restart_after_the_end_keeps_the_outcome() {
let start = Instant::now();
let mut check = EndCheck::exact(start);
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
check.depth_read(start, empty());
assert_eq!(
finish(&mut check, start, empty()),
EndAction::Ended(EndOutcome::Completed)
);
check.restart(at(start, 1));
assert_eq!(
check.next(at(start, 1)),
EndAction::Ended(EndOutcome::Completed)
);
}
#[test]
fn a_window_too_long_for_an_instant_never_closes() {
let start = Instant::now();
let mut check = EndCheck::windowed(Duration::MAX, start);
assert_eq!(check.next(start), EndAction::ReadMarker);
check.marker_read(start, present("a"));
empty_reads(&mut check, start, 0, 45);
assert_eq!(
check.next(at(start, 60)),
EndAction::ReadDepth(DepthOf::Feed)
);
}
#[test]
fn each_count_holds_the_window_open() {
let start = Instant::now();
let setters: [fn(&mut Depth); 4] = [
|d| d.visible = 1,
|d| d.delayed = 1,
|d| d.in_flight = 1,
|d| d.unsent = 1,
];
for set in setters {
let mut check = opened(start, "a");
assert_eq!(check.next(start), EndAction::ReadDepth(DepthOf::Feed));
let mut depth = Depth::new("queue", true);
set(&mut depth);
check.depth_read(start, Ok(depth));
assert_eq!(check.next(at(start, 15)), EndAction::ReadMarker);
}
}
}