use shep_client::shep_core::config::AppConfig;
use shep_client::shep_core::protocol::ProcessInfo;
use tokio::time::{Instant, sleep};
use super::attempt::cutover_budget;
use crate::daemon::Daemon;
use crate::verify::POLL;
pub(super) async fn undo_start<D: Daemon>(
daemon: &D,
sheep: &str,
previous: &[u32],
original: AppConfig,
) -> Undone {
let Ok(flock) = daemon.describe(sheep).await else {
return Undone {
removed: false,
recorded: false,
};
};
let mut removed = drain(daemon, &flock, previous).await;
let patience = cutover_budget(&original);
let Ok(repair) = daemon.start(vec![original]).await else {
return Undone {
removed,
recorded: false,
};
};
let deadline = Instant::now() + patience;
loop {
let Ok(flock) = daemon.describe(sheep).await else {
return Undone {
removed: false,
recorded: true,
};
};
let all_present = repair
.iter()
.all(|id| flock.iter().any(|info| info.id == *id));
if all_present {
removed &= drain(daemon, &flock, previous).await;
break;
}
if Instant::now() >= deadline {
removed = false;
let _ = drain(daemon, &flock, previous).await;
break;
}
sleep(POLL).await;
}
Undone {
removed,
recorded: true,
}
}
pub(super) struct Undone {
pub(super) removed: bool,
pub(super) recorded: bool,
}
async fn drain<D: Daemon>(daemon: &D, flock: &[ProcessInfo], previous: &[u32]) -> bool {
let mut all = true;
for info in flock.iter().filter(|info| !previous.contains(&info.id)) {
if daemon.delete(info.id).await.is_err() {
all = false;
}
}
all
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::error::Error;
use crate::optin::cut_over;
use crate::optin::test_support::{
NEWCOMER_ID, REPAIR_ID, cutover_fixture, cutover_fixture_dies_and_refuses_deletes,
cutover_fixture_dies_and_refuses_repair,
cutover_fixture_dies_and_refuses_the_second_delete, cutover_fixture_dies_during_dwell,
cutover_fixture_never_appears,
};
#[tokio::test(start_paused = true)]
async fn an_abandoned_cutover_puts_the_original_config_back_in_the_roll() {
let (daemon, prepared, _dirs) = cutover_fixture_dies_during_dwell().await;
let original = prepared.previous_config.clone();
cut_over(&daemon, prepared).await.expect_err("gives up");
let last = daemon.started().last().cloned().expect("a repair Start");
assert_eq!(
last.cwd, original.cwd,
"the roll is re-recorded at the old cwd"
);
assert_eq!(
daemon.deleted().len(),
2,
"the newcomer, and the instance the repair Start spawned"
);
}
#[tokio::test(start_paused = true)]
async fn a_scaled_repair_is_drained_whole_however_its_rows_arrive() {
let (daemon, mut prepared, _dirs) = cutover_fixture_dies_during_dwell().await;
let daemon = daemon.staggering_repairs_by(POLL * 4);
prepared.previous_config.instances = 3;
cut_over(&daemon, prepared).await.expect_err("gives up");
for offset in 0..3 {
assert!(
daemon.is_deleted(REPAIR_ID + offset),
"repair row {offset} left registered: {:?}",
daemon.deleted()
);
}
}
#[tokio::test(start_paused = true)]
async fn a_late_cutover_newcomer_is_not_taken_for_the_repair_instance() {
let (daemon, mut prepared, _dirs) = cutover_fixture().await;
prepared.app.instances = 2;
let late = cutover_budget(&prepared.app) + Duration::from_millis(100);
let daemon = daemon.spawning_at_most(1).with_a_late_newcomer_after(late);
cut_over(&daemon, prepared)
.await
.expect_err("gives up on the missing newcomer");
assert!(daemon.is_deleted(REPAIR_ID), "{:?}", daemon.deleted());
assert!(
daemon.is_deleted(NEWCOMER_ID - 1),
"the late newcomer goes too: {:?}",
daemon.deleted()
);
}
#[tokio::test(start_paused = true)]
async fn a_failed_roll_repair_is_named_not_glossed() {
let (daemon, prepared, _dirs) = cutover_fixture_dies_and_refuses_repair().await;
let err = cut_over(&daemon, prepared).await.expect_err("gives up");
let shown = err.to_string();
assert!(shown.contains("muster"), "{shown}");
assert!(
shown.contains("reboot") || shown.contains("restart"),
"{shown}"
);
}
#[tokio::test(start_paused = true)]
async fn a_cutover_that_added_nothing_does_not_claim_to_have_removed_it() {
let (daemon, prepared, _dirs) = cutover_fixture_never_appears().await;
let err = cut_over(&daemon, prepared).await.expect_err("gives up");
let shown = err.to_string();
assert!(shown.contains("No new instance appeared"), "{shown}");
assert!(
!shown.contains("instance it added has been removed"),
"it cannot have removed what it never added: {shown}"
);
assert!(
shown.contains("Nothing this cutover started is left registered"),
"{shown}"
);
}
#[tokio::test(start_paused = true)]
async fn a_repair_instance_left_behind_is_not_described_by_its_cwd() {
let (daemon, prepared, _dirs) = cutover_fixture_dies_and_refuses_the_second_delete().await;
let err = cut_over(&daemon, prepared).await.expect_err("gives up");
assert!(
matches!(
err,
Error::CutOver {
removed: false,
repaired: true,
..
}
),
"{err}"
);
let shown = err.to_string();
assert!(
!shown.contains("cwd under the deploy tree"),
"the hint would point at the wrong instance: {shown}"
);
assert!(shown.contains("shep delete <id>"), "{shown}");
}
#[tokio::test(start_paused = true)]
async fn a_newcomer_that_could_not_be_removed_is_named_not_claimed_gone() {
let (daemon, prepared, _dirs) = cutover_fixture_dies_and_refuses_deletes().await;
let err = cut_over(&daemon, prepared).await.expect_err("gives up");
let shown = err.to_string();
assert!(shown.contains("could NOT be removed"), "{shown}");
assert!(shown.contains("shep describe bpm"), "{shown}");
}
}