use serde::{Deserialize, Serialize};
use super::{role, EstateView};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Provider {
Gone,
Stopped,
Running,
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Main {
Silent,
Says { data: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Confirm {
pub by: String,
pub storage: String,
#[serde(default)]
pub accept_lost: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Twin {
pub addr: String,
pub hears_primary: bool,
pub recharge_possible: bool,
pub verdict: String,
pub holds: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seen {
pub provider: Provider,
pub main: Main,
#[serde(default)]
pub main_head: Option<u64>,
#[serde(default)]
pub booted_again: bool,
#[serde(default)]
pub installer_refused: Option<String>,
pub twin: Option<Twin>,
#[serde(default)]
pub data_storage: String,
#[serde(default)]
pub confirm: Option<Confirm>,
#[serde(default)]
pub auto: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "do")]
pub enum Rebuild {
Healthy { why: String },
StartIt { why: String },
BootAgain { why: String },
Fill { twin: String, why: String },
LayAgain { twin: String, why: String },
Reformat { storage: String, twin: String, why: String },
Refused { code: String, why: String },
}
impl Rebuild {
pub fn word(&self) -> &'static str {
match self {
Rebuild::Healthy { .. } => "healthy",
Rebuild::StartIt { .. } => "start_it",
Rebuild::BootAgain { .. } => "boot_again",
Rebuild::Fill { .. } => "fill",
Rebuild::LayAgain { .. } => "lay_again",
Rebuild::Reformat { .. } => "reformat",
Rebuild::Refused { .. } => "refused",
}
}
pub fn destroys(&self) -> bool {
matches!(self, Rebuild::Reformat { .. })
}
pub fn why(&self) -> &str {
match self {
Rebuild::Healthy { why }
| Rebuild::StartIt { why }
| Rebuild::BootAgain { why }
| Rebuild::Fill { why, .. }
| Rebuild::LayAgain { why, .. }
| Rebuild::Reformat { why, .. }
| Rebuild::Refused { why, .. } => why,
}
}
}
pub mod code {
pub const NO_TWIN: &str = "no_twin";
pub const TWIN_HEARS_A_MAIN: &str = "twin_hears_a_main";
pub const TWIN_NOT_READY: &str = "twin_not_ready";
pub const MAIN_AHEAD: &str = "main_ahead";
pub const DIVERGED: &str = "diverged";
pub const AUTO_NEVER_REFORMATS: &str = "auto_never_reformats";
pub const CONFIRM_STORAGE: &str = "confirm_storage";
pub const PROVIDER_UNKNOWN: &str = "provider_unknown";
}
fn refused(code: &str, why: impl Into<String>) -> Rebuild {
Rebuild::Refused { code: code.into(), why: why.into() }
}
fn twin_may_fill(s: &Seen) -> Result<String, Rebuild> {
let Some(t) = s.twin.as_ref() else {
return Err(refused(code::NO_TWIN, "no twin answered: there is nothing to rebuild the main from — a human looks first (provider console, last receipts)"));
};
if t.hears_primary {
return Err(refused(
code::TWIN_HEARS_A_MAIN,
format!("the twin at {} still hears its primary: a main it can hear is not dead — a rebuild beside it would be a second writable main", t.addr),
));
}
if !t.recharge_possible {
return Err(refused(code::TWIN_NOT_READY, format!("the twin at {} cannot give the estate back now: {}", t.addr, t.verdict)));
}
let accepted = s.confirm.as_ref().is_some_and(|c| c.accept_lost);
if let (Some(head), Some(twin_has)) = (s.main_head, t.holds) {
if head > twin_has && !accepted {
return Err(refused(
code::MAIN_AHEAD,
format!("the main last reported log head {head}, the twin holds {twin_has}: {} entr(y/ies) exist only on the main's disk — a person accepts that loss (accept_lost), or the main's disk is brought back", head - twin_has),
));
}
}
Ok(t.addr.clone())
}
pub fn judge(s: &Seen) -> Rebuild {
match &s.main {
Main::Says { data } => match data.as_str() {
"ok" => Rebuild::Healthy { why: "the main answers and reports its data whole: nothing is rebuilt".into() },
"diverged" => refused(code::DIVERGED, "the main reports its data diverged from the twin's log: a person compares the two before either is touched"),
"fresh" | "lost" => match twin_may_fill(s) {
Ok(twin) => Rebuild::Fill { twin, why: format!("the main answers and reports data={data}: it is filled from the twin, nothing on it is destroyed") },
Err(r) => r,
},
other => refused(code::PROVIDER_UNKNOWN, format!("the main reports data={other:?}, a word the guard does not act on")),
},
Main::Silent => match &s.provider {
Provider::Stopped => Rebuild::StartIt { why: "the main is stopped at the provider: start it — it keeps its disk and rejoins as it was; nothing is rebuilt".into() },
Provider::Gone => match twin_may_fill(s) {
Ok(twin) => Rebuild::LayAgain { twin, why: "the main is GONE at the provider: it is laid anew from its medium and filled from the twin (nothing of it is left to destroy)".into() },
Err(r) => r,
},
Provider::Unknown(w) => refused(code::PROVIDER_UNKNOWN, format!("the provider's word on the main is not known ({w}): nothing is destroyed on a guess")),
Provider::Running if !s.booted_again && s.installer_refused.is_none() => {
Rebuild::BootAgain { why: "the main runs at the provider and is silent: it is booted once more from its keep medium — a disk with its data comes back as it was; only a box that stays silent is judged again".into() }
}
Provider::Running => {
let twin = match twin_may_fill(s) {
Ok(t) => t,
Err(r) => return r,
};
if s.auto {
return refused(code::AUTO_NEVER_REFORMATS, "the main's server stands with its disk: auto-fix never reformats a standing disk — a person confirms it (the kit, the ONE box, or the console)");
}
let evidence = match &s.installer_refused {
Some(w) => format!("the installer refused its data set under the keep medium ({w})"),
None => "it was booted again and stays silent".to_string(),
};
match s.confirm.as_ref() {
Some(c) if !s.data_storage.is_empty() && c.storage.trim() == s.data_storage => Rebuild::Reformat {
storage: s.data_storage.clone(),
twin,
why: format!("the main's data is destroyed: {evidence}; {} confirmed storage {} — it is given back, the main laid anew and filled from the twin", c.by, s.data_storage),
},
Some(c) => refused(code::CONFIRM_STORAGE, format!("the confirmation names storage {:?}, the main's data storage is {:?}: nothing is destroyed", c.storage, s.data_storage)),
None => refused(code::CONFIRM_STORAGE, format!("the main's data looks destroyed ({evidence}); a person confirms by typing its data storage back: {}", if s.data_storage.is_empty() { "(unknown)" } else { &s.data_storage })),
}
}
},
}
}
pub fn provider_of(word: &str) -> Provider {
match word.trim() {
"gone" => Provider::Gone,
"stopped" | "maintenance" => Provider::Stopped,
"active" | "started" | "running" | "provisioning" => Provider::Running,
"" => Provider::Unknown("the provider was not asked".into()),
other => Provider::Unknown(other.into()),
}
}
pub fn main_of(v: &EstateView) -> (Main, Option<u64>) {
let b = v.get(role::MAIN);
let head = b.and_then(|b| b.word("data_head").trim().parse().ok());
if v.silent(role::MAIN) {
return (Main::Silent, head);
}
let data = match b.map(|b| b.word("data")).unwrap_or("") {
"restoring" => "lost",
w => w,
};
(Main::Says { data: data.into() }, head)
}
pub fn twin_addr(v: &EstateView) -> String {
v.get(role::TWIN).map(|b| if b.word("serves_at").is_empty() { b.laid_ipv4.clone() } else { b.word("serves_at").to_string() }).unwrap_or_default()
}
pub fn seen_of(v: &EstateView, twin: Option<Twin>, auto: bool) -> Seen {
let (main, main_head) = main_of(v);
let provider = provider_of(v.get(role::MAIN).map(|b| b.at_provider.as_str()).unwrap_or(""));
Seen { provider, main, main_head, booted_again: false, installer_refused: None, twin, data_storage: String::new(), confirm: None, auto }
}
pub fn auto_new_server(v: &EstateView, twin: Option<Twin>) -> Rebuild {
judge(&seen_of(v, twin, true))
}
pub fn new_server(v: &EstateView, twin: Option<Twin>) -> Result<String, String> {
let mut s = seen_of(v, twin, false);
let operator = v.get(role::MAIN).is_none_or(|b| b.at_provider.trim().is_empty());
if operator && s.main == Main::Silent {
s.provider = Provider::Gone;
}
match judge(&s) {
Rebuild::LayAgain { why, .. } if operator => Ok(format!("{why} — on this estate the operator's kit asks the provider and confirms before anything is destroyed")),
Rebuild::LayAgain { why, .. } => Ok(why),
Rebuild::Fill { why, .. } => Err(format!("{why}: press Recharge from twin — New server would destroy a box that answers")),
other => Err(other.why().to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::guardian::BoxView;
const STORAGE: &str = "01aa-data";
const TWIN_AT: &str = "10.0.0.3:50051";
fn twin(hears: bool, possible: bool, holds: u64) -> Twin {
Twin { addr: TWIN_AT.into(), hears_primary: hears, recharge_possible: possible, verdict: if possible { "level".into() } else { "behind its primary".into() }, holds: Some(holds) }
}
fn seen(provider: Provider, main: Main) -> Seen {
Seen { provider, main, main_head: Some(100), booted_again: false, installer_refused: None, twin: Some(twin(false, true, 100)), data_storage: STORAGE.into(), confirm: None, auto: false }
}
fn says(d: &str) -> Main {
Main::Says { data: d.into() }
}
fn me(storage: &str) -> Option<Confirm> {
Some(Confirm { by: "rickard".into(), storage: storage.into(), accept_lost: false })
}
#[test]
fn a_main_with_its_data_is_never_rebuilt() {
for p in [Provider::Running, Provider::Gone, Provider::Stopped] {
let mut s = seen(p, says("ok"));
s.confirm = me(STORAGE);
assert_eq!(judge(&s).word(), "healthy");
assert!(!judge(&s).destroys());
}
}
#[test]
fn silence_alone_destroys_nothing() {
assert_eq!(judge(&seen(Provider::Stopped, Main::Silent)).word(), "start_it");
let mut s = seen(Provider::Running, Main::Silent);
s.confirm = me(STORAGE);
assert_eq!(judge(&s).word(), "boot_again", "even a confirmation does not skip booting it again");
assert_eq!(judge(&seen(Provider::Unknown("timeout".into()), Main::Silent)).word(), "refused");
}
#[test]
fn a_main_that_says_it_is_empty_is_filled_and_auto_may() {
for d in ["fresh", "lost"] {
let mut s = seen(Provider::Running, says(d));
s.auto = true;
match judge(&s) {
Rebuild::Fill { twin, .. } => assert_eq!(twin, TWIN_AT),
other => panic!("{d}: {other:?}"),
}
}
assert_eq!(judge(&seen(Provider::Running, says("diverged"))).word(), "refused");
}
#[test]
fn a_gone_main_is_laid_again_and_auto_may() {
let mut s = seen(Provider::Gone, Main::Silent);
s.auto = true;
assert_eq!(judge(&s).word(), "lay_again");
}
#[test]
fn a_standing_disk_is_reformatted_only_on_a_typed_confirmation() {
let mut s = seen(Provider::Running, Main::Silent);
s.booted_again = true;
let r = judge(&s);
assert!(matches!(&r, Rebuild::Refused { code, .. } if code == code::CONFIRM_STORAGE), "{r:?}");
assert!(r.why().contains(STORAGE), "the person is told which storage: {r:?}");
s.confirm = me("01bb-other");
assert!(matches!(judge(&s), Rebuild::Refused { code, .. } if code == code::CONFIRM_STORAGE), "another storage's name is no confirmation");
s.confirm = me(STORAGE);
match judge(&s) {
Rebuild::Reformat { storage, twin, .. } => assert_eq!((storage.as_str(), twin.as_str()), (STORAGE, TWIN_AT)),
other => panic!("{other:?}"),
}
s.auto = true;
assert!(matches!(judge(&s), Rebuild::Refused { code, .. } if code == code::AUTO_NEVER_REFORMATS), "auto-fix never, even confirmed");
let mut i = seen(Provider::Running, Main::Silent);
i.installer_refused = Some("member 1 of 2 is missing".into());
i.confirm = me(STORAGE);
assert_eq!(judge(&i).word(), "reformat", "the installer's refusal of the data set is evidence too");
i.data_storage.clear();
assert_eq!(judge(&i).word(), "refused", "an unknown storage cannot be confirmed");
}
#[test]
fn the_twin_must_be_able_to_give_it_back() {
for (t, want) in [(None, code::NO_TWIN), (Some(twin(true, true, 100)), code::TWIN_HEARS_A_MAIN), (Some(twin(false, false, 90)), code::TWIN_NOT_READY)] {
for (p, m) in [(Provider::Gone, Main::Silent), (Provider::Running, says("fresh"))] {
let mut s = seen(p, m);
s.twin = t.clone();
assert!(matches!(judge(&s), Rebuild::Refused { ref code, .. } if code == want), "{want}: {:?}", judge(&s));
}
}
}
#[test]
fn a_main_ahead_of_its_twin_needs_a_person_to_accept_the_loss() {
let mut s = seen(Provider::Gone, Main::Silent);
s.main_head = Some(120);
let r = judge(&s);
assert!(matches!(&r, Rebuild::Refused { code, .. } if code == code::MAIN_AHEAD), "{r:?}");
assert!(r.why().contains("20 entr"), "{r:?}");
s.confirm = Some(Confirm { by: "rickard".into(), storage: String::new(), accept_lost: true });
assert_eq!(judge(&s).word(), "lay_again");
s.main_head = None;
s.confirm = None;
assert_eq!(judge(&s).word(), "lay_again", "an unknown head is not 'ahead'");
s.main_head = Some(120);
s.twin.as_mut().unwrap().holds = None;
assert_eq!(judge(&s).word(), "lay_again", "a twin with no log head to compare (Holger) is not 'behind'");
}
#[test]
fn only_a_reformat_destroys() {
assert!(Rebuild::Reformat { storage: STORAGE.into(), twin: "t".into(), why: "w".into() }.destroys());
for r in [Rebuild::Fill { twin: "t".into(), why: String::new() }, Rebuild::LayAgain { twin: "t".into(), why: String::new() }, Rebuild::BootAgain { why: String::new() }, Rebuild::StartIt { why: String::new() }] {
assert!(!r.destroys());
}
}
const NOW: u64 = 1_800_000_000_000;
fn estate(main_heard_ago: u64, main_words: &[(&str, &str)], at_provider: &str) -> EstateView {
let health = main_words.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
let main = BoxView { role: "main".into(), heard_unix_ms: NOW - main_heard_ago, health, at_provider: at_provider.into(), ..Default::default() };
EstateView { product: "p".into(), instance: "acme".into(), now_unix_ms: NOW, silent_after_ms: 90_000, boxes: [("main".to_string(), main)].into_iter().collect() }
}
fn blind() -> Option<Twin> {
Some(Twin { addr: TWIN_AT.into(), hears_primary: false, recharge_possible: true, verdict: "level".into(), holds: None })
}
#[test]
fn the_providers_word_is_read_and_empty_is_not_asked() {
assert_eq!(provider_of("gone"), Provider::Gone);
assert_eq!(provider_of("stopped"), Provider::Stopped);
assert_eq!(provider_of("active"), Provider::Running);
assert!(matches!(provider_of(""), Provider::Unknown(_)));
assert!(matches!(provider_of("unknown (timeout)"), Provider::Unknown(_)));
}
#[test]
fn the_main_is_read_off_its_reports() {
assert_eq!(main_of(&estate(600_000, &[("data", "ok"), ("data_head", "900")], "")), (Main::Silent, Some(900)));
assert_eq!(main_of(&estate(1_000, &[("data", "restoring")], "")), (says("lost"), None));
assert_eq!(main_of(&estate(1_000, &[("data", "fresh"), ("data_head", "0")], "")), (says("fresh"), Some(0)));
}
#[test]
fn auto_fix_lays_a_silent_main_again_only_when_the_provider_says_it_is_gone() {
assert_eq!(auto_new_server(&estate(600_000, &[], "gone"), blind()).word(), "lay_again");
assert_eq!(auto_new_server(&estate(600_000, &[], "active"), blind()).word(), "boot_again");
assert_eq!(auto_new_server(&estate(600_000, &[], "stopped"), blind()).word(), "start_it");
assert_eq!(auto_new_server(&estate(600_000, &[], ""), blind()).word(), "refused", "not asked is not gone");
assert_eq!(auto_new_server(&estate(600_000, &[], "gone"), None).word(), "refused", "no twin");
}
#[test]
fn a_persons_new_server_destroys_no_standing_disk() {
assert!(new_server(&estate(600_000, &[], "gone"), blind()).is_ok());
let standing = new_server(&estate(600_000, &[], "active"), blind()).unwrap_err();
assert!(standing.contains("booted once more"), "{standing}");
let answers = new_server(&estate(1_000, &[("data", "fresh")], "active"), blind()).unwrap_err();
assert!(answers.contains("Recharge from twin"), "{answers}");
assert!(new_server(&estate(1_000, &[("data", "ok")], "active"), blind()).is_err(), "a healthy main is never laid again");
let operator = new_server(&estate(600_000, &[], ""), blind()).unwrap();
assert!(operator.contains("operator's kit"), "{operator}");
let mut hears = blind();
hears.as_mut().unwrap().hears_primary = true;
assert!(new_server(&estate(600_000, &[], ""), hears).is_err(), "the twin still hears its main: not on an operator's estate either");
}
}