#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaintenanceState {
Loading,
Serving,
Rebuilding,
Stopping,
Failed,
}
impl MaintenanceState {
pub fn as_str(self) -> &'static str {
match self {
MaintenanceState::Loading => "loading",
MaintenanceState::Serving => "serving",
MaintenanceState::Rebuilding => "rebuilding",
MaintenanceState::Stopping => "stopping",
MaintenanceState::Failed => "failed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdmissionClosed {
pub state: MaintenanceState,
}
impl std::fmt::Display for AdmissionClosed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "server unavailable: engine is {}", self.state.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebuildRefused {
NotReady,
Busy(MaintenanceState),
Latched,
}
impl std::fmt::Display for RebuildRefused {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RebuildRefused::NotReady => {
write!(f, "model is still loading; cannot rebuild cache yet")
}
RebuildRefused::Busy(MaintenanceState::Stopping) => {
write!(f, "engine stop is in progress")
}
RebuildRefused::Busy(_) => write!(f, "a cache rebuild is already in progress"),
RebuildRefused::Latched => {
write!(f, "server latched in maintenance; restart required")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopRefused {
RebuildInProgress,
UnidentifiedInflight(usize),
AbortBarrierTimedOut(usize),
}
impl std::fmt::Display for StopRefused {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StopRefused::RebuildInProgress => {
write!(
f,
"cache rebuild is in progress; retry stop after it finishes"
)
}
StopRefused::UnidentifiedInflight(n) => write!(
f,
"accounting drain timed out with {n} unidentified request(s)"
),
StopRefused::AbortBarrierTimedOut(n) => write!(
f,
"accounting abort barrier timed out with {n} request(s) still active"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SealedAccounting {
pub model_id: Option<String>,
pub prompt_tokens_total: u64,
pub completion_tokens_total: u64,
pub uptime_seconds: u64,
pub drain_complete: bool,
}
#[derive(Debug, Clone)]
pub struct MaintenanceGate {
state: MaintenanceState,
sealed: Option<SealedAccounting>,
}
impl Default for MaintenanceGate {
fn default() -> Self {
Self::new()
}
}
impl MaintenanceGate {
pub fn new() -> Self {
MaintenanceGate {
state: MaintenanceState::Loading,
sealed: None,
}
}
pub fn serving() -> Self {
MaintenanceGate {
state: MaintenanceState::Serving,
sealed: None,
}
}
pub fn state(&self) -> MaintenanceState {
self.state
}
pub fn check_admission(&self) -> Result<(), AdmissionClosed> {
if self.state == MaintenanceState::Serving {
Ok(())
} else {
Err(AdmissionClosed { state: self.state })
}
}
pub fn finish_loading(&mut self, ok: bool) {
self.state = if ok {
MaintenanceState::Serving
} else {
MaintenanceState::Failed
};
}
pub fn latch_failed(&mut self) {
self.state = MaintenanceState::Failed;
}
pub fn begin_rebuild(&mut self) -> Result<(), RebuildRefused> {
match self.state {
MaintenanceState::Serving => {
self.state = MaintenanceState::Rebuilding;
Ok(())
}
MaintenanceState::Loading => Err(RebuildRefused::NotReady),
MaintenanceState::Failed => Err(RebuildRefused::Latched),
other => Err(RebuildRefused::Busy(other)),
}
}
pub fn rebuild_never_dispatched(&mut self) {
if self.state == MaintenanceState::Rebuilding {
self.state = MaintenanceState::Serving;
}
}
pub fn finish_rebuild(&mut self, ok: bool) {
if self.state == MaintenanceState::Rebuilding {
self.state = if ok {
MaintenanceState::Serving
} else {
MaintenanceState::Failed
};
}
}
pub fn rebuild_timed_out(&self) {}
pub fn begin_stop(&mut self) -> Result<(), StopRefused> {
if self.state == MaintenanceState::Rebuilding {
return Err(StopRefused::RebuildInProgress);
}
self.state = MaintenanceState::Stopping;
Ok(())
}
pub fn sealed(&self) -> Option<&SealedAccounting> {
self.sealed.as_ref()
}
pub fn seal(
&mut self,
active: usize,
had_identities: bool,
snapshot: impl FnOnce() -> SealedAccounting,
) -> Result<SealedAccounting, StopRefused> {
if let Some(sealed) = &self.sealed {
return Ok(sealed.clone());
}
if active > 0 {
return Err(if had_identities {
StopRefused::AbortBarrierTimedOut(active)
} else {
StopRefused::UnidentifiedInflight(active)
});
}
let sealed = snapshot();
self.sealed = Some(sealed.clone());
Ok(sealed)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn snapshot() -> SealedAccounting {
SealedAccounting {
model_id: Some("m".to_string()),
prompt_tokens_total: 10,
completion_tokens_total: 20,
uptime_seconds: 30,
drain_complete: true,
}
}
fn serving() -> MaintenanceGate {
let mut gate = MaintenanceGate::new();
gate.finish_loading(true);
gate
}
#[test]
fn an_engine_starts_closed_and_opens_only_when_loading_succeeds() {
let mut gate = MaintenanceGate::new();
assert!(gate.check_admission().is_err());
gate.finish_loading(true);
assert!(gate.check_admission().is_ok());
let mut gate = MaintenanceGate::new();
gate.finish_loading(false);
assert_eq!(gate.state(), MaintenanceState::Failed);
assert!(gate.check_admission().is_err());
}
#[test]
fn only_serving_admits_and_the_refusal_names_the_state() {
let mut gate = serving();
gate.begin_rebuild().expect("rebuild starts");
let err = gate.check_admission().expect_err("rebuilding must refuse");
assert_eq!(err.to_string(), "server unavailable: engine is rebuilding");
}
#[test]
fn a_rebuild_timeout_leaves_the_gate_closed() {
let mut gate = serving();
gate.begin_rebuild().expect("rebuild starts");
gate.rebuild_timed_out();
assert_eq!(gate.state(), MaintenanceState::Rebuilding);
assert!(gate.check_admission().is_err());
gate.finish_rebuild(true);
assert!(gate.check_admission().is_ok());
}
#[test]
fn a_rebuild_that_never_dispatched_reopens_the_gate() {
let mut gate = serving();
gate.begin_rebuild().expect("rebuild starts");
gate.rebuild_never_dispatched();
assert_eq!(gate.state(), MaintenanceState::Serving);
assert!(gate.check_admission().is_ok());
}
#[test]
fn a_refused_rebuild_says_which_kind_of_no_it_is() {
let mut loading = MaintenanceGate::new();
assert_eq!(loading.begin_rebuild(), Err(RebuildRefused::NotReady));
let mut latched = serving();
latched.latch_failed();
assert_eq!(latched.begin_rebuild(), Err(RebuildRefused::Latched));
let mut busy = serving();
busy.begin_rebuild().expect("first");
assert_eq!(
busy.begin_rebuild(),
Err(RebuildRefused::Busy(MaintenanceState::Rebuilding))
);
let mut stopping = serving();
stopping.begin_stop().expect("stop starts");
assert_eq!(
stopping.begin_rebuild(),
Err(RebuildRefused::Busy(MaintenanceState::Stopping))
);
}
#[test]
fn a_stop_cannot_start_through_a_rebuild() {
let mut gate = serving();
gate.begin_rebuild().expect("rebuild starts");
assert_eq!(gate.begin_stop(), Err(StopRefused::RebuildInProgress));
assert_eq!(gate.state(), MaintenanceState::Rebuilding);
}
#[test]
fn a_drained_stop_seals() {
let mut gate = serving();
gate.begin_stop().expect("stop starts");
assert_eq!(gate.seal(0, false, snapshot), Ok(snapshot()));
assert_eq!(gate.sealed(), Some(&snapshot()));
}
#[test]
fn an_abort_barrier_timeout_seals_nothing_and_reopens_nothing() {
let mut gate = serving();
gate.begin_stop().expect("stop starts");
assert_eq!(
gate.seal(2, true, snapshot),
Err(StopRefused::AbortBarrierTimedOut(2))
);
assert_eq!(gate.state(), MaintenanceState::Stopping);
assert!(gate.check_admission().is_err());
assert_eq!(gate.sealed(), None);
assert_eq!(gate.seal(0, true, snapshot), Ok(snapshot()));
}
#[test]
fn an_active_count_with_no_identities_is_its_own_refusal() {
let mut gate = serving();
gate.begin_stop().expect("stop starts");
assert_eq!(
gate.seal(1, false, snapshot),
Err(StopRefused::UnidentifiedInflight(1))
);
}
#[test]
fn sealing_is_idempotent_for_the_life_of_the_process() {
let mut gate = serving();
gate.begin_stop().expect("stop starts");
let first = gate.seal(0, false, snapshot).expect("seals");
let second = gate
.seal(0, false, || SealedAccounting {
completion_tokens_total: 99_999,
..snapshot()
})
.expect("seals again");
assert_eq!(first, second, "a retry must not re-measure");
}
}