use statig::prelude::*;
use super::types::VmLifecycleState;
type Outcome = statig::Outcome<State>;
#[derive(Debug, Clone)]
pub(super) enum VmEvent {
Start {
create: bool,
timeout_ms: u64,
},
Activity,
AgentReady,
Failure,
IdleTimeout,
Stop,
Stopped,
ForceStop,
GuestReset {
timeout_ms: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Notify {
Started,
Idle,
Stopped,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Effect {
SpawnBoot {
create: bool,
timeout_ms: u64,
},
SpawnStop,
SpawnReboot {
timeout_ms: u64,
},
AbortInflight,
RemoveMachine,
BumpGeneration,
Publish(Notify),
ReadyWaiters,
FailWaiters(String),
}
#[derive(Debug, Default)]
pub(super) struct Effects {
items: Vec<Effect>,
}
impl Effects {
fn emit(&mut self, effect: Effect) {
self.items.push(effect);
}
pub(super) fn take(&mut self) -> Vec<Effect> {
std::mem::take(&mut self.items)
}
fn force_stop(&mut self) -> Outcome {
self.emit(Effect::AbortInflight);
self.emit(Effect::RemoveMachine);
self.emit(Effect::BumpGeneration);
self.emit(Effect::Publish(Notify::Stopped));
self.emit(Effect::FailWaiters("force stopped".to_owned()));
Transition(State::not_exist())
}
}
#[derive(Debug, Default)]
pub(super) struct VmLifecycle;
#[state_machine(
initial = "State::not_exist()",
state(derive(Debug, Clone, Copy, PartialEq, Eq)),
superstate(derive(Debug))
)]
impl VmLifecycle {
#[superstate]
fn managed(event: &VmEvent, context: &mut Effects) -> Outcome {
match event {
VmEvent::ForceStop => context.force_stop(),
VmEvent::Failure => Transition(State::failed()),
_ => Handled,
}
}
#[superstate(superstate = "managed")]
fn resting(event: &VmEvent, context: &mut Effects) -> Outcome {
match event {
VmEvent::Start { create, timeout_ms } => {
context.emit(Effect::SpawnBoot {
create: *create,
timeout_ms: *timeout_ms,
});
Transition(if *create {
State::creating()
} else {
State::starting()
})
}
_ => Super,
}
}
#[state(superstate = "resting")]
fn not_exist() -> Outcome {
Super
}
#[state(superstate = "resting")]
fn created() -> Outcome {
Super
}
#[state(superstate = "resting")]
fn stopped() -> Outcome {
Super
}
#[state(superstate = "resting")]
fn failed() -> Outcome {
Super
}
#[superstate(superstate = "managed")]
fn booting(event: &VmEvent, context: &mut Effects) -> Outcome {
match event {
VmEvent::AgentReady => {
context.emit(Effect::Publish(Notify::Started));
context.emit(Effect::ReadyWaiters);
Transition(State::running())
}
_ => Super,
}
}
#[state(superstate = "booting")]
fn creating() -> Outcome {
Super
}
#[state(superstate = "booting")]
fn starting() -> Outcome {
Super
}
#[superstate(superstate = "managed")]
fn active(event: &VmEvent, context: &mut Effects) -> Outcome {
match event {
VmEvent::Stop => {
context.emit(Effect::SpawnStop);
Transition(State::stopping())
}
VmEvent::GuestReset { timeout_ms } => {
context.emit(Effect::BumpGeneration);
context.emit(Effect::SpawnReboot {
timeout_ms: *timeout_ms,
});
Transition(State::starting())
}
_ => Super,
}
}
#[state(superstate = "active")]
fn running(event: &VmEvent, context: &mut Effects) -> Outcome {
match event {
VmEvent::IdleTimeout => {
context.emit(Effect::Publish(Notify::Idle));
Transition(State::idle())
}
VmEvent::Activity | VmEvent::Start { .. } => Handled,
_ => Super,
}
}
#[state(superstate = "active")]
fn idle(event: &VmEvent) -> Outcome {
match event {
VmEvent::Activity | VmEvent::Start { .. } => Transition(State::running()),
_ => Super,
}
}
#[state(superstate = "managed")]
fn stopping(event: &VmEvent, context: &mut Effects) -> Outcome {
match event {
VmEvent::Stopped => {
context.emit(Effect::BumpGeneration);
context.emit(Effect::Publish(Notify::Stopped));
Transition(State::stopped())
}
_ => Super,
}
}
}
impl State {
pub(super) fn to_public(self) -> VmLifecycleState {
match self {
Self::NotExist {} => VmLifecycleState::NotExist,
Self::Creating {} => VmLifecycleState::Creating,
Self::Created {} => VmLifecycleState::Created,
Self::Starting {} => VmLifecycleState::Starting,
Self::Running {} => VmLifecycleState::Running,
Self::Idle {} => VmLifecycleState::Idle,
Self::Stopping {} => VmLifecycleState::Stopping,
Self::Stopped {} => VmLifecycleState::Stopped,
Self::Failed {} => VmLifecycleState::Failed,
}
}
}
#[cfg(test)]
mod machine_tests {
use super::*;
type Machine = statig::blocking::InitializedStateMachine<VmLifecycle>;
fn machine(fx: &mut Effects) -> Machine {
VmLifecycle
.uninitialized_state_machine()
.init_with_context(fx)
}
fn step(sm: &mut Machine, fx: &mut Effects, event: VmEvent) -> (VmLifecycleState, Vec<Effect>) {
sm.handle_with_context(&event, fx);
(sm.state().to_public(), fx.take())
}
const T: u64 = 90_000;
fn start(create: bool) -> VmEvent {
VmEvent::Start {
create,
timeout_ms: T,
}
}
#[test]
fn start_from_not_exist_creates_then_boots_to_running() {
let mut fx = Effects::default();
let mut sm = machine(&mut fx);
assert_eq!(sm.state().to_public(), VmLifecycleState::NotExist);
let (state, effects) = step(&mut sm, &mut fx, start(true));
assert_eq!(state, VmLifecycleState::Creating);
assert_eq!(
effects,
vec![Effect::SpawnBoot {
create: true,
timeout_ms: T
}]
);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::AgentReady);
assert_eq!(state, VmLifecycleState::Running);
assert_eq!(
effects,
vec![Effect::Publish(Notify::Started), Effect::ReadyWaiters]
);
}
#[test]
fn start_without_create_goes_straight_to_starting() {
let mut fx = Effects::default();
let mut sm = machine(&mut fx);
let (state, effects) = step(&mut sm, &mut fx, start(false));
assert_eq!(state, VmLifecycleState::Starting);
assert_eq!(
effects,
vec![Effect::SpawnBoot {
create: false,
timeout_ms: T
}]
);
}
#[test]
fn boot_failure_lands_in_failed() {
let mut fx = Effects::default();
let mut sm = machine(&mut fx);
step(&mut sm, &mut fx, start(true));
let (state, effects) = step(&mut sm, &mut fx, VmEvent::Failure);
assert_eq!(state, VmLifecycleState::Failed);
assert!(effects.is_empty());
}
#[test]
fn guest_reset_reboots_from_running() {
let mut fx = Effects::default();
let mut sm = running_machine(&mut fx);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::GuestReset { timeout_ms: T });
assert_eq!(state, VmLifecycleState::Starting);
assert_eq!(
effects,
vec![
Effect::BumpGeneration,
Effect::SpawnReboot { timeout_ms: T }
]
);
let (state, _) = step(&mut sm, &mut fx, VmEvent::AgentReady);
assert_eq!(state, VmLifecycleState::Running);
}
#[test]
fn guest_reset_from_idle_also_reboots() {
let mut fx = Effects::default();
let mut sm = running_machine(&mut fx);
step(&mut sm, &mut fx, VmEvent::IdleTimeout);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::GuestReset { timeout_ms: T });
assert_eq!(state, VmLifecycleState::Starting);
assert_eq!(
effects,
vec![
Effect::BumpGeneration,
Effect::SpawnReboot { timeout_ms: T }
]
);
}
#[test]
fn idle_round_trip_returns_to_running_on_activity() {
let mut fx = Effects::default();
let mut sm = running_machine(&mut fx);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::IdleTimeout);
assert_eq!(state, VmLifecycleState::Idle);
assert_eq!(effects, vec![Effect::Publish(Notify::Idle)]);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::Activity);
assert_eq!(state, VmLifecycleState::Running);
assert!(effects.is_empty());
}
#[test]
fn activity_while_running_is_a_noop() {
let mut fx = Effects::default();
let mut sm = running_machine(&mut fx);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::Activity);
assert_eq!(state, VmLifecycleState::Running);
assert!(effects.is_empty());
}
#[test]
fn stop_from_running_drains_through_stopping() {
let mut fx = Effects::default();
let mut sm = running_machine(&mut fx);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::Stop);
assert_eq!(state, VmLifecycleState::Stopping);
assert_eq!(effects, vec![Effect::SpawnStop]);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::Stopped);
assert_eq!(state, VmLifecycleState::Stopped);
assert_eq!(
effects,
vec![Effect::BumpGeneration, Effect::Publish(Notify::Stopped)]
);
}
#[test]
fn stop_while_booting_is_swallowed_for_actor_deferral() {
let mut fx = Effects::default();
let mut sm = machine(&mut fx);
step(&mut sm, &mut fx, start(false));
let (state, effects) = step(&mut sm, &mut fx, VmEvent::Stop);
assert_eq!(state, VmLifecycleState::Starting);
assert!(effects.is_empty());
}
#[test]
fn force_stop_preempts_from_every_phase() {
for reach in [
ReachState::Creating,
ReachState::Running,
ReachState::Idle,
ReachState::Stopping,
] {
let mut fx = Effects::default();
let mut sm = machine(&mut fx);
reach.drive(&mut sm, &mut fx);
let (state, effects) = step(&mut sm, &mut fx, VmEvent::ForceStop);
assert_eq!(
state,
VmLifecycleState::NotExist,
"force stop from {reach:?} must reach NotExist"
);
assert_eq!(
effects,
vec![
Effect::AbortInflight,
Effect::RemoveMachine,
Effect::BumpGeneration,
Effect::Publish(Notify::Stopped),
Effect::FailWaiters("force stopped".to_owned()),
]
);
}
}
fn running_machine(fx: &mut Effects) -> Machine {
let mut sm = machine(fx);
sm.handle_with_context(&start(true), fx);
sm.handle_with_context(&VmEvent::AgentReady, fx);
fx.take();
sm
}
#[derive(Debug, Clone, Copy)]
enum ReachState {
Creating,
Running,
Idle,
Stopping,
}
impl ReachState {
fn drive(self, sm: &mut Machine, fx: &mut Effects) {
sm.handle_with_context(&start(true), fx);
match self {
Self::Creating => {}
Self::Running => {
sm.handle_with_context(&VmEvent::AgentReady, fx);
}
Self::Idle => {
sm.handle_with_context(&VmEvent::AgentReady, fx);
sm.handle_with_context(&VmEvent::IdleTimeout, fx);
}
Self::Stopping => {
sm.handle_with_context(&VmEvent::AgentReady, fx);
sm.handle_with_context(&VmEvent::Stop, fx);
}
}
fx.take();
}
}
}