mod dwell;
mod generation;
#[cfg(test)]
mod test_support;
use std::time::Duration;
use shep_client::shep_core::protocol::ProcessInfo;
use shep_client::shep_core::status::ProcStatus;
use tokio::time::{Instant, sleep};
use crate::daemon::Daemon;
use crate::error::Error;
use crate::state::Verify;
pub use generation::Generation;
pub(crate) const DWELL: Duration = Duration::from_secs(10);
pub(crate) const POLL: Duration = Duration::from_millis(100);
pub(crate) const MAX_WAIT: Duration = Duration::from_secs(60 * 60);
#[must_use]
pub(crate) fn bounded(budget: Duration) -> Duration {
budget.min(MAX_WAIT)
}
pub async fn wait<D: Daemon>(
daemon: &D,
sheep: &str,
mode: Verify,
before: &Generation,
budget: Duration,
dwell: Duration,
) -> Result<bool, Error> {
let accept = match mode {
Verify::Probed => is_online,
Verify::Alive => is_alive,
};
let started = Instant::now();
let Some(settled) = turnover(daemon, sheep, before, accept, budget).await? else {
return Ok(false);
};
match mode {
Verify::Probed => dwell::probed(daemon, sheep, &settled, dwell).await,
Verify::Alive => dwell::alive(daemon, sheep, &settled, started + budget).await,
}
}
async fn describe_within<D: Daemon>(
daemon: &D,
sheep: &str,
budget: Duration,
) -> Result<Vec<ProcessInfo>, Error> {
let deadline = Instant::now() + budget;
loop {
match daemon.describe(sheep).await {
Ok(flock) => return Ok(flock),
Err(err) if !err.is_retryable() => return Err(err),
Err(err) => {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(err);
}
sleep(POLL.min(left)).await;
}
}
}
}
async fn turnover<D: Daemon>(
daemon: &D,
sheep: &str,
before: &Generation,
accept: fn(&ProcessInfo) -> bool,
budget: Duration,
) -> Result<Option<Generation>, Error> {
let deadline = Instant::now() + budget;
loop {
let flock = describe_within(
daemon,
sheep,
deadline.saturating_duration_since(Instant::now()),
)
.await?;
if before.has_turned_over(&flock, accept) {
return Ok(Some(Generation::of_infos(
&flock.iter().collect::<Vec<_>>(),
)));
}
if Instant::now() >= deadline {
return Ok(None);
}
sleep(POLL).await;
}
}
fn is_online(info: &ProcessInfo) -> bool {
info.status == ProcStatus::Online
}
pub(crate) fn is_alive(info: &ProcessInfo) -> bool {
matches!(info.status, ProcStatus::Starting | ProcStatus::Online)
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use shep_client::shep_core::status::ProcStatus;
use super::*;
use test_support::{Blips, Listings, instance, serving, serving_all};
#[tokio::test]
async fn a_transient_describe_failure_does_not_fail_verification() {
let before = serving(111);
let daemon = Blips {
calls: Cell::new(0),
after: vec![instance(0, ProcStatus::Online, 222)],
};
let seen = turnover(&daemon, "web", &before, is_online, Duration::from_secs(10))
.await
.expect("a retryable blip must not end verification");
assert!(
seen.is_some(),
"the turnover after the blip must still be seen"
);
assert!(daemon.calls.get() >= 2, "it must have asked again");
}
#[tokio::test(start_paused = true)]
async fn the_old_instance_still_online_is_not_a_new_release() {
let daemon = Listings::new(vec![vec![instance(1, ProcStatus::Online, 12835)]]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(50),
DWELL,
)
.await
.unwrap();
assert!(!ok);
}
#[tokio::test(start_paused = true)]
async fn a_reload_that_has_not_happened_yet_is_not_success() {
let daemon = Listings::new(vec![
vec![instance(1, ProcStatus::Online, 12835)],
vec![instance(1, ProcStatus::Online, 12835)],
]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(50),
DWELL,
)
.await
.unwrap();
assert!(!ok);
}
#[tokio::test(start_paused = true)]
async fn a_new_instance_reaching_online_is_success() {
let daemon = Listings::new(vec![
vec![instance(1, ProcStatus::Online, 12835)],
vec![
instance(1, ProcStatus::Stopping, 12835),
instance(2, ProcStatus::Starting, 13002),
],
vec![instance(2, ProcStatus::Online, 13002)],
]);
assert!(
wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_secs(5),
DWELL
)
.await
.unwrap()
);
}
#[tokio::test(start_paused = true)]
async fn a_draining_old_instance_is_not_a_finished_turnover() {
let daemon = Listings::new(vec![vec![
instance(1, ProcStatus::Stopping, 12835),
instance(2, ProcStatus::Online, 13002),
]]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(50),
DWELL,
)
.await
.unwrap();
assert!(!ok);
}
#[tokio::test(start_paused = true)]
async fn a_new_instance_that_never_leaves_starting_is_not_success() {
let daemon = Listings::new(vec![vec![instance(2, ProcStatus::Starting, 13002)]]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(50),
DWELL,
)
.await
.unwrap();
assert!(!ok);
}
#[tokio::test(start_paused = true)]
async fn a_turnover_that_lost_a_replica_is_not_a_turnover() {
let daemon = Listings::new(vec![vec![instance(1, ProcStatus::Online, 200)]]);
let before = serving_all(&[100, 101]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&before,
Duration::from_millis(50),
DWELL,
)
.await
.unwrap();
assert!(!ok, "one replacement for two instances is not a turnover");
}
#[tokio::test(start_paused = true)]
async fn an_instance_with_no_pid_is_not_a_new_generation() {
let daemon = Listings::new(vec![vec![
ProcessInfo::builder(2, "web", ProcStatus::Online).build(),
]]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(50),
DWELL,
)
.await
.unwrap();
assert!(!ok);
}
#[tokio::test(start_paused = true)]
async fn an_empty_describe_is_failure_in_both_modes() {
let empty = Listings::new(Vec::new());
assert!(
!wait(
&empty,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(50),
DWELL
)
.await
.unwrap()
);
let empty = Listings::new(Vec::new());
assert!(
!wait(
&empty,
"web",
Verify::Alive,
&serving(12835),
Duration::from_millis(50),
DWELL
)
.await
.unwrap()
);
}
#[tokio::test(start_paused = true)]
async fn the_fixture_repeats_its_last_listing_past_exhaustion() {
let daemon = Listings::new(vec![
vec![instance(1, ProcStatus::Online, 12835)],
vec![instance(1, ProcStatus::Online, 12835)],
]);
let ok = wait(
&daemon,
"web",
Verify::Probed,
&serving(12835),
Duration::from_millis(350),
DWELL,
)
.await
.unwrap();
assert!(!ok);
}
#[tokio::test(start_paused = true)]
async fn a_replica_between_restarts_before_the_reload_still_counts() {
let capture = Listings::new(vec![vec![
instance(1, ProcStatus::Online, 100),
instance(2, ProcStatus::Online, 101),
ProcessInfo::builder(3, "web", ProcStatus::WaitingRestart).build(),
]]);
let before = Generation::of(&capture, "web").await.unwrap();
let daemon = Listings::new(vec![vec![
instance(1, ProcStatus::Online, 200),
instance(2, ProcStatus::Online, 201),
instance(3, ProcStatus::Online, 202),
]]);
assert!(
wait(
&daemon,
"web",
Verify::Probed,
&before,
Duration::from_secs(5),
DWELL
)
.await
.unwrap(),
"three rows replaced by three new pids is a turnover"
);
}
#[test]
fn a_budget_is_held_under_the_ceiling() {
assert_eq!(bounded(Duration::MAX), MAX_WAIT);
assert_eq!(bounded(Duration::from_secs(5)), Duration::from_secs(5));
let _ = Instant::now() + MAX_WAIT;
}
}