use core::time::Duration;
use std::fmt;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ChangeSetError {
#[error("invalid changeset transition: from={from:?} to={to:?}")]
InvalidTransition {
from: ChangeSetState,
to: ChangeSetState,
},
#[error("prerequisite not met: {0}")]
PrerequisiteNotMet(&'static str),
#[error("resource proof failed: {0}")]
ResourceProofFailed(&'static str),
#[error("changeset timeout: {0:?}")]
Timeout(Duration),
#[error("changeset not timed out yet: elapsed={0:?}")]
NotTimedOut(Duration),
#[error("generation conflict: current={current}, expected={expected}")]
GenerationConflict {
current: u64,
expected: u64,
},
#[error("changeset already finalized")]
Finalized,
#[error("shadow verification failed: {0}")]
ShadowVerificationFailed(&'static str),
#[error("stop old admission failed: {0}")]
StopAdmissionFailed(&'static str),
#[error("drain failed: {0}")]
DrainFailed(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChangeSetState {
Prepare,
Validate,
ResourceProof,
Shadow,
Activate,
StopOldAdmission,
Drain,
Commit,
Rollback,
}
impl ChangeSetState {
#[inline]
pub fn can_transition_to(self, to: ChangeSetState) -> bool {
use ChangeSetState::*;
matches!(
(self, to),
(Prepare, Validate)
| (Validate, ResourceProof)
| (ResourceProof, Shadow)
| (Shadow, Activate)
| (Activate, StopOldAdmission)
| (StopOldAdmission, Drain)
| (Drain, Commit)
| (Activate, Rollback)
| (StopOldAdmission, Rollback)
| (Drain, Rollback)
| (Prepare, Rollback)
| (Validate, Rollback)
| (ResourceProof, Rollback)
| (Shadow, Rollback)
| (Commit, Prepare)
| (Rollback, Prepare)
)
}
#[inline]
pub fn is_terminal(self) -> bool {
matches!(self, ChangeSetState::Commit | ChangeSetState::Rollback)
}
}
impl fmt::Display for ChangeSetState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ChangeSetState::Prepare => write!(f, "Prepare"),
ChangeSetState::Validate => write!(f, "Validate"),
ChangeSetState::ResourceProof => write!(f, "ResourceProof"),
ChangeSetState::Shadow => write!(f, "Shadow"),
ChangeSetState::Activate => write!(f, "Activate"),
ChangeSetState::StopOldAdmission => write!(f, "StopOldAdmission"),
ChangeSetState::Drain => write!(f, "Drain"),
ChangeSetState::Commit => write!(f, "Commit"),
ChangeSetState::Rollback => write!(f, "Rollback"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeSetOutcome {
Committed {
generation: u64,
},
RolledBack {
generation: u64,
},
}
#[derive(Debug, Clone)]
pub struct ChangeSet {
state: ChangeSetState,
target_generation: u64,
active_generation: u64,
previous_generation: u64,
timeout: Duration,
steps_completed: u32,
rolled_back: bool,
}
impl ChangeSet {
pub fn new(active_generation: u64, target_generation: u64) -> Self {
Self {
state: ChangeSetState::Prepare,
target_generation,
active_generation,
previous_generation: active_generation,
timeout: Duration::from_secs(30),
steps_completed: 0,
rolled_back: false,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[inline]
pub fn state(&self) -> ChangeSetState {
self.state
}
#[inline]
pub fn target_generation(&self) -> u64 {
self.target_generation
}
#[inline]
pub fn active_generation(&self) -> u64 {
self.active_generation
}
#[inline]
pub fn steps_completed(&self) -> u32 {
self.steps_completed
}
#[inline]
pub fn is_finalized(&self) -> bool {
self.state.is_terminal()
}
fn transition(
&mut self,
to: ChangeSetState,
) -> Result<(), ChangeSetError> {
if self.state.is_terminal() {
return Err(ChangeSetError::Finalized);
}
if !self.state.can_transition_to(to) {
return Err(ChangeSetError::InvalidTransition { from: self.state, to });
}
self.state = to;
self.steps_completed = self.steps_completed.saturating_add(1);
Ok(())
}
fn check_generation(&self, expected: u64) -> Result<(), ChangeSetError> {
if self.active_generation != expected {
return Err(ChangeSetError::GenerationConflict {
current: self.active_generation,
expected,
});
}
Ok(())
}
pub fn prepare(&mut self) -> Result<(), ChangeSetError> {
if self.state == ChangeSetState::Prepare {
self.steps_completed = self.steps_completed.saturating_add(1);
return Ok(());
}
if self.state.is_terminal() {
self.state = ChangeSetState::Prepare;
self.steps_completed = 1;
self.rolled_back = false;
return Ok(());
}
Err(ChangeSetError::InvalidTransition {
from: self.state,
to: ChangeSetState::Prepare,
})
}
pub fn validate(&mut self) -> Result<(), ChangeSetError> {
self.transition(ChangeSetState::Validate)
}
pub fn resource_proof<F>(&mut self, check_fn: F) -> Result<(), ChangeSetError>
where
F: FnOnce() -> Result<(), &'static str>,
{
let prev_state = self.state;
self.transition(ChangeSetState::ResourceProof)?;
if let Err(e) = check_fn() {
self.state = prev_state;
return Err(ChangeSetError::ResourceProofFailed(e));
}
Ok(())
}
pub fn shadow<F>(&mut self, verify_fn: F) -> Result<(), ChangeSetError>
where
F: FnOnce() -> Result<(), &'static str>,
{
let prev_state = self.state;
self.transition(ChangeSetState::Shadow)?;
if let Err(e) = verify_fn() {
self.state = prev_state;
return Err(ChangeSetError::ShadowVerificationFailed(e));
}
Ok(())
}
pub fn activate(
&mut self,
expected_generation: u64,
) -> Result<(), ChangeSetError> {
self.check_generation(expected_generation)?;
self.transition(ChangeSetState::Activate)?;
self.previous_generation = self.active_generation;
self.active_generation = self.target_generation;
Ok(())
}
pub fn stop_old_admission(&mut self) -> Result<(), ChangeSetError> {
self.transition(ChangeSetState::StopOldAdmission)
}
pub fn drain<F>(&mut self, drain_fn: F) -> Result<(), ChangeSetError>
where
F: FnOnce() -> Result<Option<u64>, &'static str>,
{
let prev_state = self.state;
self.transition(ChangeSetState::Drain)?;
let remaining = drain_fn().map_err(|e| {
self.state = prev_state;
ChangeSetError::DrainFailed(e)
})?;
if remaining.is_some() {
self.state = prev_state;
return Err(ChangeSetError::DrainFailed("drain not fully complete"));
}
Ok(())
}
pub fn commit(&mut self) -> Result<ChangeSetOutcome, ChangeSetError> {
self.transition(ChangeSetState::Commit)?;
Ok(ChangeSetOutcome::Committed {
generation: self.target_generation,
})
}
pub fn rollback(
&mut self,
to_generation: Option<u64>,
) -> Result<ChangeSetOutcome, ChangeSetError> {
if !self.state.is_terminal() && self.state != ChangeSetState::Prepare {
self.state = ChangeSetState::Rollback;
self.steps_completed = self.steps_completed.saturating_add(1);
} else if self.state == ChangeSetState::Prepare {
self.state = ChangeSetState::Rollback;
self.steps_completed = self.steps_completed.saturating_add(1);
} else {
return Err(ChangeSetError::Finalized);
}
let target = to_generation.unwrap_or(self.previous_generation);
self.active_generation = target;
self.rolled_back = true;
Ok(ChangeSetOutcome::RolledBack { generation: target })
}
pub fn check_timeout(&self, elapsed: Duration) -> Result<(), ChangeSetError> {
if elapsed > self.timeout {
return Err(ChangeSetError::Timeout(self.timeout));
}
Ok(())
}
pub fn rollback_on_timeout(
&mut self,
elapsed: Duration,
) -> Result<ChangeSetOutcome, ChangeSetError> {
match self.check_timeout(elapsed) {
Ok(()) => Err(ChangeSetError::NotTimedOut(elapsed)),
Err(ChangeSetError::Timeout(_)) => self.rollback(None),
Err(e) => Err(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_changeset_initial_state() {
let cs = ChangeSet::new(1, 2);
assert_eq!(cs.state(), ChangeSetState::Prepare);
assert_eq!(cs.active_generation(), 1);
assert_eq!(cs.target_generation(), 2);
assert!(!cs.is_finalized());
}
#[test]
fn test_full_happy_path() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
assert_eq!(cs.active_generation(), 2);
cs.stop_old_admission().unwrap();
cs.drain(|| Ok(None)).unwrap();
let outcome = cs.commit().unwrap();
assert!(matches!(outcome, ChangeSetOutcome::Committed { generation: 2 }));
assert!(cs.is_finalized());
}
#[test]
fn test_invalid_transition() {
let mut cs = ChangeSet::new(1, 2);
let err = cs.activate(1).unwrap_err();
assert!(matches!(
err,
ChangeSetError::InvalidTransition {
from: ChangeSetState::Prepare,
to: ChangeSetState::Activate,
}
));
}
#[test]
fn test_resource_proof_failure() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
let err = cs
.resource_proof(|| Err("out of memory"))
.unwrap_err();
assert!(matches!(err, ChangeSetError::ResourceProofFailed("out of memory")));
assert_eq!(cs.state(), ChangeSetState::Validate);
cs.resource_proof(|| Ok(())).unwrap();
assert_eq!(cs.state(), ChangeSetState::ResourceProof);
}
#[test]
fn test_shadow_failure() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
let err = cs.shadow(|| Err("hash mismatch")).unwrap_err();
assert!(matches!(
err,
ChangeSetError::ShadowVerificationFailed("hash mismatch")
));
assert_eq!(cs.state(), ChangeSetState::ResourceProof);
cs.shadow(|| Ok(())).unwrap();
assert_eq!(cs.state(), ChangeSetState::Shadow);
}
#[test]
fn test_activate_generation_conflict() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
let err = cs.activate(99).unwrap_err();
assert!(matches!(
err,
ChangeSetError::GenerationConflict {
current: 1,
expected: 99,
}
));
}
#[test]
fn test_rollback_from_activate() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
let outcome = cs.rollback(Some(1)).unwrap();
assert!(matches!(
outcome,
ChangeSetOutcome::RolledBack { generation: 1 }
));
assert!(cs.is_finalized());
}
#[test]
fn test_rollback_default_generation() {
let mut cs = ChangeSet::new(10, 15);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(10).unwrap();
let outcome = cs.rollback(None).unwrap();
assert!(matches!(
outcome,
ChangeSetOutcome::RolledBack { generation: 10 }
));
assert_eq!(cs.active_generation(), 10);
}
#[test]
fn test_rollback_on_timeout_actually_rolls_back() {
let mut cs = ChangeSet::new(3, 7).with_timeout(Duration::from_millis(5));
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(3).unwrap();
let outcome = cs.rollback_on_timeout(Duration::from_millis(10)).unwrap();
assert!(matches!(
outcome,
ChangeSetOutcome::RolledBack { generation: 3 }
));
assert_eq!(cs.active_generation(), 3);
assert!(cs.is_finalized());
}
#[test]
fn test_rollback_on_timeout_not_timed_out() {
let mut cs = ChangeSet::new(1, 2).with_timeout(Duration::from_secs(30));
cs.prepare().unwrap();
cs.validate().unwrap();
let err = cs
.rollback_on_timeout(Duration::from_millis(1))
.unwrap_err();
assert!(matches!(err, ChangeSetError::NotTimedOut(_)));
assert_eq!(cs.state(), ChangeSetState::Validate);
assert!(!cs.is_finalized());
}
#[test]
fn test_drain_failure() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
cs.stop_old_admission().unwrap();
let err = cs.drain(|| Ok(Some(42))).unwrap_err();
assert!(matches!(err, ChangeSetError::DrainFailed("drain not fully complete")));
}
#[test]
fn test_timeout() {
let cs = ChangeSet::new(1, 2).with_timeout(Duration::from_millis(1));
let elapsed = Duration::from_millis(10);
let err = cs.check_timeout(elapsed).unwrap_err();
assert!(matches!(err, ChangeSetError::Timeout(_)));
}
#[test]
fn test_steps_counter() {
let mut cs = ChangeSet::new(1, 2);
assert_eq!(cs.steps_completed(), 0);
cs.prepare().unwrap();
assert_eq!(cs.steps_completed(), 1);
cs.validate().unwrap();
assert_eq!(cs.steps_completed(), 2);
}
#[test]
fn test_prepare_idempotent() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.prepare().unwrap(); assert_eq!(cs.state(), ChangeSetState::Prepare);
}
#[test]
fn test_prepare_after_finalized() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
cs.stop_old_admission().unwrap();
cs.drain(|| Ok(None)).unwrap();
let _ = cs.commit().unwrap();
assert!(cs.is_finalized());
cs.prepare().unwrap();
assert_eq!(cs.state(), ChangeSetState::Prepare);
assert!(!cs.is_finalized());
}
#[test]
fn test_changeset_state_display() {
let states = [
ChangeSetState::Prepare,
ChangeSetState::Validate,
ChangeSetState::ResourceProof,
ChangeSetState::Shadow,
ChangeSetState::Activate,
ChangeSetState::StopOldAdmission,
ChangeSetState::Drain,
ChangeSetState::Commit,
ChangeSetState::Rollback,
];
for state in states.iter() {
let display = format!("{}", state);
assert!(!display.is_empty());
}
}
#[test]
fn test_changeset_state_is_terminal() {
assert!(ChangeSetState::Commit.is_terminal());
assert!(ChangeSetState::Rollback.is_terminal());
assert!(!ChangeSetState::Prepare.is_terminal());
assert!(!ChangeSetState::Activate.is_terminal());
assert!(!ChangeSetState::Drain.is_terminal());
}
#[test]
fn test_changeset_state_can_transition() {
use ChangeSetState::*;
assert!(Prepare.can_transition_to(Validate));
assert!(Validate.can_transition_to(ResourceProof));
assert!(ResourceProof.can_transition_to(Shadow));
assert!(Shadow.can_transition_to(Activate));
assert!(Activate.can_transition_to(StopOldAdmission));
assert!(StopOldAdmission.can_transition_to(Drain));
assert!(Drain.can_transition_to(Commit));
assert!(Activate.can_transition_to(Rollback));
assert!(Commit.can_transition_to(Prepare));
assert!(Rollback.can_transition_to(Prepare));
assert!(!Prepare.can_transition_to(Activate));
assert!(!Commit.can_transition_to(Activate));
}
#[test]
fn test_changeset_outcome_debug() {
let committed = ChangeSetOutcome::Committed { generation: 42 };
let debug_str = format!("{:?}", committed);
assert!(!debug_str.is_empty());
assert!(debug_str.contains("42"));
let rolled_back = ChangeSetOutcome::RolledBack { generation: 10 };
let debug_str = format!("{:?}", rolled_back);
assert!(!debug_str.is_empty());
assert!(debug_str.contains("10"));
}
#[test]
fn test_changeset_error_display() {
let err = ChangeSetError::Timeout(Duration::from_secs(30));
let display = format!("{}", err);
assert!(!display.is_empty());
assert!(display.contains("timeout"));
let err = ChangeSetError::Finalized;
let display = format!("{}", err);
assert!(!display.is_empty());
}
#[test]
fn test_changeset_error_debug() {
let err = ChangeSetError::InvalidTransition {
from: ChangeSetState::Prepare,
to: ChangeSetState::Commit,
};
let debug_str = format!("{:?}", err);
assert!(!debug_str.is_empty());
}
#[test]
fn test_changeset_with_timeout() {
let cs = ChangeSet::new(1, 2).with_timeout(Duration::from_secs(60));
let result = cs.check_timeout(Duration::from_secs(30));
assert!(result.is_ok());
let result = cs.check_timeout(Duration::from_secs(90));
assert!(result.is_err());
}
#[test]
fn test_rollback_from_prepare() {
let mut cs = ChangeSet::new(5, 10);
let outcome = cs.rollback(None).unwrap();
assert!(matches!(outcome, ChangeSetOutcome::RolledBack { .. }));
assert!(cs.is_finalized());
}
#[test]
fn test_rollback_from_validate() {
let mut cs = ChangeSet::new(5, 10);
cs.prepare().unwrap();
cs.validate().unwrap();
let outcome = cs.rollback(Some(5)).unwrap();
assert!(matches!(outcome, ChangeSetOutcome::RolledBack { generation: 5 }));
assert!(cs.is_finalized());
}
#[test]
fn test_rollback_when_already_finalized() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
cs.stop_old_admission().unwrap();
cs.drain(|| Ok(None)).unwrap();
let _ = cs.commit().unwrap();
assert!(cs.is_finalized());
let result = cs.rollback(None);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ChangeSetError::Finalized));
}
#[test]
fn test_drain_error_propagation() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
cs.stop_old_admission().unwrap();
let err = cs.drain(|| Err("drain function failed")).unwrap_err();
assert!(matches!(err, ChangeSetError::DrainFailed("drain function failed")));
}
#[test]
fn test_stop_old_admission_from_activate() {
let mut cs = ChangeSet::new(1, 2);
cs.prepare().unwrap();
cs.validate().unwrap();
cs.resource_proof(|| Ok(())).unwrap();
cs.shadow(|| Ok(())).unwrap();
cs.activate(1).unwrap();
assert_eq!(cs.state(), ChangeSetState::Activate);
cs.stop_old_admission().unwrap();
assert_eq!(cs.state(), ChangeSetState::StopOldAdmission);
}
#[test]
fn test_rolled_back_flag() {
let mut cs = ChangeSet::new(1, 2);
assert!(!cs.rolled_back);
let _ = cs.rollback(None);
assert!(cs.rolled_back);
}
}