use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FabricStrategy {
pub name: String,
pub consensus: ConsensusConfig,
pub membership: MembershipConfig,
pub content: ContentConfig,
pub attestation: AttestationConfig,
pub placement: PlacementPolicy,
pub reconciliation: ReconciliationCadence,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsensusConfig {
pub kind: ConsensusKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "algorithm", rename_all = "kebab-case")]
pub enum ConsensusKind {
OpenRaft {
quorum_size: u32,
election_timeout_ms: u32,
snapshot_interval_entries: u32,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MembershipConfig {
pub phi_threshold: f64,
pub gossip_cadence_ms: u32,
pub failure_detector_timeout_ms: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContentConfig {
pub sync_cadence_ms: u32,
pub gc_retention_ms: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttestationConfig {
pub seal_interval_ms: u32,
pub require_operator_signature: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum PlacementPolicy {
ZoneAware { min_zones: u32 },
RackAware { min_racks: u32 },
LatencyAware { max_p99_ms: u32 },
Spread,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReconciliationCadence {
millis: u32,
}
impl ReconciliationCadence {
pub fn new(millis: u32) -> Result<Self, FabricStrategyError> {
if millis == 0 {
return Err(FabricStrategyError::ZeroReconciliationCadence);
}
Ok(Self { millis })
}
#[must_use]
pub const fn as_duration(self) -> Duration {
Duration::from_millis(self.millis as u64)
}
#[must_use]
pub const fn millis(self) -> u32 {
self.millis
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum FabricStrategyError {
#[error("reconciliation cadence cannot be zero — would break the always-eventually-resolves guarantee")]
ZeroReconciliationCadence,
#[error("failure-detector timeout ({fd_ms}ms) >= reconciliation cadence ({rec_ms}ms) — a partition may not heal before the next convergence round, breaking liveness")]
DetectorOutpacesReconciliation { fd_ms: u32, rec_ms: u32 },
#[error("consensus quorum size must be odd to avoid even-split deadlocks; got {0}")]
EvenQuorum(u32),
#[error("consensus quorum size must be ≥ 3 for split-brain freedom under single-node failure; got {0}")]
QuorumTooSmall(u32),
}
impl FabricStrategy {
pub fn prove_liveness(&self) -> Result<(), FabricStrategyError> {
let rec_ms = self.reconciliation.millis();
if rec_ms == 0 {
return Err(FabricStrategyError::ZeroReconciliationCadence);
}
if self.membership.failure_detector_timeout_ms >= rec_ms {
return Err(FabricStrategyError::DetectorOutpacesReconciliation {
fd_ms: self.membership.failure_detector_timeout_ms,
rec_ms,
});
}
match self.consensus.kind {
ConsensusKind::OpenRaft { quorum_size, .. } => {
if quorum_size < 3 {
return Err(FabricStrategyError::QuorumTooSmall(quorum_size));
}
if quorum_size % 2 == 0 {
return Err(FabricStrategyError::EvenQuorum(quorum_size));
}
}
}
Ok(())
}
#[must_use]
pub fn prescribed_homelab() -> Self {
Self {
name: "homelab-3node".into(),
consensus: ConsensusConfig {
kind: ConsensusKind::OpenRaft {
quorum_size: 3,
election_timeout_ms: 150,
snapshot_interval_entries: 1000,
},
},
membership: MembershipConfig {
phi_threshold: 8.0,
gossip_cadence_ms: 100,
failure_detector_timeout_ms: 300,
},
content: ContentConfig {
sync_cadence_ms: 200,
gc_retention_ms: 60_000,
},
attestation: AttestationConfig {
seal_interval_ms: 1000,
require_operator_signature: false,
},
placement: PlacementPolicy::ZoneAware { min_zones: 1 },
reconciliation: ReconciliationCadence::new(500)
.expect("500ms is non-zero"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FabricFace {
pub name: String,
pub kind: FaceKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "protocol", rename_all = "kebab-case")]
pub enum FaceKind {
Kubernetes {
version: String,
certified_cncf: bool,
},
Nomad { version: String },
Systemd { user_units: bool },
PureRaft,
BareMetalSupervisor,
}
impl FabricFace {
#[must_use]
pub fn prescribed_kubernetes_v1_34() -> Self {
Self {
name: "k8s-v1.34".into(),
kind: FaceKind::Kubernetes {
version: "1.34".into(),
certified_cncf: true,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prescribed_homelab_proves_liveness() {
let s = FabricStrategy::prescribed_homelab();
assert_eq!(s.prove_liveness(), Ok(()));
}
#[test]
fn zero_reconciliation_cadence_cannot_be_constructed() {
let err = ReconciliationCadence::new(0).unwrap_err();
assert_eq!(err, FabricStrategyError::ZeroReconciliationCadence);
}
#[test]
fn non_zero_reconciliation_cadence_round_trips_through_duration() {
let c = ReconciliationCadence::new(250).unwrap();
assert_eq!(c.as_duration(), Duration::from_millis(250));
assert_eq!(c.millis(), 250);
}
#[test]
fn detector_must_be_strictly_faster_than_reconciliation() {
let mut s = FabricStrategy::prescribed_homelab();
s.membership.failure_detector_timeout_ms = s.reconciliation.millis();
let err = s.prove_liveness().unwrap_err();
assert!(matches!(
err,
FabricStrategyError::DetectorOutpacesReconciliation { .. }
));
}
#[test]
fn quorum_must_be_odd() {
let mut s = FabricStrategy::prescribed_homelab();
s.consensus.kind = ConsensusKind::OpenRaft {
quorum_size: 4,
election_timeout_ms: 150,
snapshot_interval_entries: 1000,
};
let err = s.prove_liveness().unwrap_err();
assert_eq!(err, FabricStrategyError::EvenQuorum(4));
}
#[test]
fn quorum_must_be_at_least_three() {
let mut s = FabricStrategy::prescribed_homelab();
s.consensus.kind = ConsensusKind::OpenRaft {
quorum_size: 1,
election_timeout_ms: 150,
snapshot_interval_entries: 1000,
};
let err = s.prove_liveness().unwrap_err();
assert_eq!(err, FabricStrategyError::QuorumTooSmall(1));
}
#[test]
fn strategy_round_trips_through_serde() {
let s = FabricStrategy::prescribed_homelab();
let yaml = serde_yaml::to_string(&s).unwrap();
let back: FabricStrategy = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(s, back);
}
#[test]
fn prescribed_k8s_face_is_v1_34_certified() {
let f = FabricFace::prescribed_kubernetes_v1_34();
match f.kind {
FaceKind::Kubernetes {
ref version,
certified_cncf,
} => {
assert_eq!(version, "1.34");
assert!(certified_cncf);
}
other => panic!("expected Kubernetes face, got {other:?}"),
}
}
#[test]
fn nomad_face_carries_its_version() {
let f = FabricFace {
name: "nomad-1.7".into(),
kind: FaceKind::Nomad {
version: "1.7".into(),
},
};
assert!(matches!(f.kind, FaceKind::Nomad { .. }));
}
#[test]
fn pure_raft_face_has_no_external_protocol() {
let f = FabricFace {
name: "pure-raft".into(),
kind: FaceKind::PureRaft,
};
assert_eq!(f.kind, FaceKind::PureRaft);
}
#[test]
fn face_round_trips_through_serde() {
let f = FabricFace::prescribed_kubernetes_v1_34();
let yaml = serde_yaml::to_string(&f).unwrap();
let back: FabricFace = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(f, back);
}
#[test]
fn all_face_kinds_round_trip() {
let kinds = vec![
FaceKind::Kubernetes {
version: "1.34".into(),
certified_cncf: true,
},
FaceKind::Nomad {
version: "1.7".into(),
},
FaceKind::Systemd { user_units: false },
FaceKind::PureRaft,
FaceKind::BareMetalSupervisor,
];
for k in kinds {
let f = FabricFace {
name: "x".into(),
kind: k.clone(),
};
let yaml = serde_yaml::to_string(&f).unwrap();
let back: FabricFace = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(f, back, "round-trip failed for {k:?}");
}
}
}