use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TurnAdmissionPhase {
Idle,
Admitted,
Running,
Completing,
ShuttingDown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TurnAdmissionError {
pub(crate) from: TurnAdmissionPhase,
pub(crate) op: &'static str,
}
impl fmt::Display for TurnAdmissionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"illegal turn-admission operation {:?} from phase {:?}",
self.op, self.from
)
}
}
impl std::error::Error for TurnAdmissionError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FinalizeOutcome {
pub(crate) next_phase: TurnAdmissionPhase,
}
#[derive(Debug, Clone)]
pub(crate) struct TurnAdmissionSlot {
phase: TurnAdmissionPhase,
interrupt_pending: bool,
shutdown_pending: bool,
}
impl TurnAdmissionSlot {
pub(crate) fn new() -> Self {
Self {
phase: TurnAdmissionPhase::Idle,
interrupt_pending: false,
shutdown_pending: false,
}
}
pub(crate) fn phase(&self) -> TurnAdmissionPhase {
self.phase
}
pub(crate) fn interrupt_pending(&self) -> bool {
self.interrupt_pending
}
#[cfg(test)]
pub(crate) fn is_active(&self) -> bool {
matches!(
self.phase,
TurnAdmissionPhase::Admitted
| TurnAdmissionPhase::Running
| TurnAdmissionPhase::Completing
)
}
pub(crate) fn claim(&mut self) -> Result<TurnAdmissionPhase, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Idle => {
self.interrupt_pending = false;
self.shutdown_pending = false;
self.phase = TurnAdmissionPhase::Admitted;
Ok(self.phase)
}
from => Err(TurnAdmissionError { from, op: "claim" }),
}
}
pub(crate) fn abort_claim(&mut self) -> Result<TurnAdmissionPhase, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Admitted => {
self.interrupt_pending = false;
self.shutdown_pending = false;
self.phase = TurnAdmissionPhase::Idle;
Ok(self.phase)
}
from => Err(TurnAdmissionError {
from,
op: "abort_claim",
}),
}
}
pub(crate) fn begin(&mut self) -> Result<TurnAdmissionPhase, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Admitted => {
self.phase = TurnAdmissionPhase::Running;
Ok(self.phase)
}
from => Err(TurnAdmissionError { from, op: "begin" }),
}
}
pub(crate) fn resolve(&mut self) -> Result<TurnAdmissionPhase, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Running => {
self.phase = TurnAdmissionPhase::Completing;
Ok(self.phase)
}
from => Err(TurnAdmissionError {
from,
op: "resolve",
}),
}
}
pub(crate) fn finalize(&mut self) -> Result<FinalizeOutcome, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Completing => {
self.interrupt_pending = false;
if self.shutdown_pending {
self.phase = TurnAdmissionPhase::ShuttingDown;
} else {
self.shutdown_pending = false;
self.phase = TurnAdmissionPhase::Idle;
}
Ok(FinalizeOutcome {
next_phase: self.phase,
})
}
from => Err(TurnAdmissionError {
from,
op: "finalize",
}),
}
}
pub(crate) fn request_interrupt(&mut self) -> Result<bool, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Admitted | TurnAdmissionPhase::Running => {
let already_pending = self.interrupt_pending;
self.interrupt_pending = true;
Ok(!already_pending)
}
from => Err(TurnAdmissionError {
from,
op: "request_interrupt",
}),
}
}
pub(crate) fn request_shutdown(&mut self) -> Result<TurnAdmissionPhase, TurnAdmissionError> {
match self.phase {
TurnAdmissionPhase::Idle | TurnAdmissionPhase::Admitted => {
self.interrupt_pending = false;
self.shutdown_pending = true;
self.phase = TurnAdmissionPhase::ShuttingDown;
Ok(self.phase)
}
TurnAdmissionPhase::Running | TurnAdmissionPhase::Completing => {
self.shutdown_pending = true;
Ok(self.phase)
}
TurnAdmissionPhase::ShuttingDown => Ok(self.phase),
}
}
}
impl Default for TurnAdmissionSlot {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn claim_reserves_slot() {
let mut slot = TurnAdmissionSlot::new();
let phase = slot.claim().expect("idle session should admit a turn");
assert_eq!(phase, TurnAdmissionPhase::Admitted);
assert!(slot.is_active());
}
#[test]
fn interrupt_allowed_after_turn_admission() {
let mut slot = TurnAdmissionSlot::new();
let err = slot
.request_interrupt()
.expect_err("idle session cannot be interrupted");
assert_eq!(err.from, TurnAdmissionPhase::Idle);
slot.claim().unwrap();
let admitted_woke = slot
.request_interrupt()
.expect("admitted session should accept interrupt");
assert!(admitted_woke);
assert!(slot.interrupt_pending());
slot.begin().unwrap();
let woke = slot
.request_interrupt()
.expect("running session should retain interrupt");
assert!(!woke);
assert_eq!(slot.phase(), TurnAdmissionPhase::Running);
assert!(slot.interrupt_pending());
}
#[test]
fn shutdown_gracefully_drains_running_turn() {
let mut slot = TurnAdmissionSlot::new();
slot.claim().unwrap();
slot.begin().unwrap();
slot.request_shutdown().unwrap();
assert_eq!(slot.phase(), TurnAdmissionPhase::Running);
slot.resolve().unwrap();
let outcome = slot
.finalize()
.expect("finalize should enter shutting down");
assert_eq!(outcome.next_phase, TurnAdmissionPhase::ShuttingDown);
}
#[test]
fn shutdown_cancels_admitted_before_run() {
let mut slot = TurnAdmissionSlot::new();
slot.claim().unwrap();
let phase = slot
.request_shutdown()
.expect("admitted turn should be shut down before run");
assert_eq!(phase, TurnAdmissionPhase::ShuttingDown);
}
}