SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
c01fd1c verified
Raw History Blame Contribute Delete
5.81 kB
// Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
//! The execution-state machine for something actually running: a
//! compute instance, a container, a function invocation.
//!
//! This is deliberately separate from [`cloud_lifecycle::Lifecycle`],
//! which governs whether a resource *record* exists at all
//! (`Creating`/`Active`/.../`Deleted`). [`RuntimeState`] governs
//! whether the thing that record represents is currently executing,
//! and the two vary independently: a resource can sit `Active` in
//! `cloud-lifecycle` terms for its whole existence while cycling
//! between `Running` and `Stopped` in `cloud-runtime` terms many times
//! over -- exactly the distinction `CLOUD_ARCHITECTURE.md`'s Phase 1
//! section cited when it deferred this crate: "a runtime only becomes
//! a real, distinct primitive once something is actually executing."
#![forbid(unsafe_code)]
use cloud_errors::CloudError;
/// A running thing's execution state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RuntimeState {
Pending,
Running,
Stopping,
Stopped,
Terminating,
Terminated,
}
impl RuntimeState {
fn name(self) -> &'static str {
match self {
RuntimeState::Pending => "Pending",
RuntimeState::Running => "Running",
RuntimeState::Stopping => "Stopping",
RuntimeState::Stopped => "Stopped",
RuntimeState::Terminating => "Terminating",
RuntimeState::Terminated => "Terminated",
}
}
/// Whether moving from `self` to `to` is allowed.
///
/// The table, spelled out rather than computed:
///
/// - `Pending` -> `Running`, `Terminating`
/// - `Running` -> `Stopping`, `Terminating`
/// - `Stopping` -> `Stopped`, `Terminating`
/// - `Stopped` -> `Pending` (restart), `Terminating`
/// - `Terminating` -> `Terminated`
/// - `Terminated` -> nothing; terminal.
pub fn can_transition_to(self, to: RuntimeState) -> bool {
use RuntimeState::*;
matches!(
(self, to),
(Pending, Running)
| (Pending, Terminating)
| (Running, Stopping)
| (Running, Terminating)
| (Stopping, Stopped)
| (Stopping, Terminating)
| (Stopped, Pending)
| (Stopped, Terminating)
| (Terminating, Terminated)
)
}
/// Attempts the transition, returning [`CloudError::InvalidTransition`]
/// if it isn't allowed.
pub fn transition_to(self, to: RuntimeState) -> Result<RuntimeState, CloudError> {
if self.can_transition_to(to) {
Ok(to)
} else {
Err(CloudError::InvalidTransition {
what: "runtime",
from: self.name().to_string(),
to: to.name().to_string(),
})
}
}
/// A terminal state has no outgoing transitions at all.
pub fn is_terminal(self) -> bool {
self == RuntimeState::Terminated
}
}
#[cfg(test)]
mod tests {
use super::*;
use RuntimeState::*;
const ALL: [RuntimeState; 6] = [Pending, Running, Stopping, Stopped, Terminating, Terminated];
#[test]
fn terminated_is_the_only_terminal_state() {
for state in ALL {
assert_eq!(state.is_terminal(), state == Terminated);
}
}
#[test]
fn terminated_has_no_outgoing_transitions() {
for target in ALL {
assert!(
!Terminated.can_transition_to(target),
"Terminated -> {target:?} should be rejected"
);
}
}
#[test]
fn the_happy_path_pending_through_terminated_is_allowed() {
let mut state = Pending;
for next in [
Running,
Stopping,
Stopped,
Pending,
Running,
Terminating,
Terminated,
] {
state = state.transition_to(next).unwrap();
}
assert_eq!(state, Terminated);
}
#[test]
fn a_running_instance_can_be_stopped_and_restarted() {
assert!(Running.can_transition_to(Stopping));
assert!(Stopping.can_transition_to(Stopped));
assert!(Stopped.can_transition_to(Pending));
assert!(Pending.can_transition_to(Running));
}
#[test]
fn every_non_terminal_state_can_move_toward_termination() {
for state in [Pending, Running, Stopping, Stopped] {
assert!(
state.can_transition_to(Terminating),
"{state:?} -> Terminating should be allowed"
);
}
assert!(Terminating.can_transition_to(Terminated));
}
#[test]
fn pending_cannot_skip_straight_to_stopping_or_stopped() {
assert!(!Pending.can_transition_to(Stopping));
assert!(!Pending.can_transition_to(Stopped));
}
#[test]
fn running_cannot_go_back_to_pending_without_stopping_first() {
assert!(!Running.can_transition_to(Pending));
}
#[test]
fn transition_to_returns_a_descriptive_error_on_rejection() {
let err = Terminated.transition_to(Running).unwrap_err();
let message = err.to_string();
assert!(message.contains("Terminated"));
assert!(message.contains("Running"));
}
#[test]
fn no_state_can_transition_to_itself() {
for state in ALL {
assert!(
!state.can_transition_to(state),
"{state:?} -> itself should not be a no-op transition"
);
}
}
}