// file: crates/ksp-job-api/src/lifecycle.rs // version: 1 /// Completion classification of a Job that reached its normal terminal state. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum JobCompletion { /// Every selected candidate or unit of work reached a durable complete result. Complete, /// The Job terminated normally while retaining one or more explicitly observable gaps. Partial, } impl JobCompletion { /// Returns the stable safe code for this completion classification. #[must_use] pub const fn code(&self) -> &'static str { return match self { Self::Complete => "complete", Self::Partial => "partial", }; } } /// Current lifecycle state of one bounded Job. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum JobState { /// The Job was admitted but has not started producing work. Created, /// The Job is actively producing or draining normal work. Running, /// Cancellation was observed and new work must no longer be admitted. Cancelling, /// The Job reached a normal terminal state with an explicit completion classification. Completed(crate::JobCompletion), /// The Job reached its cooperative cancellation terminal state. Cancelled, /// The Job reached a terminal failure state. Failed, } impl JobState { /// Returns the stable safe lifecycle code without rendering Job data. #[must_use] pub const fn code(&self) -> &'static str { return match self { Self::Created => "created", Self::Running => "running", Self::Cancelling => "cancelling", Self::Completed(_) => "completed", Self::Cancelled => "cancelled", Self::Failed => "failed", }; } /// Returns the normal completion classification when the state is [`Self::Completed`]. #[must_use] pub const fn completion(&self) -> std::option::Option { return match self { Self::Completed(completion) => std::option::Option::Some(*completion), _ => std::option::Option::None, }; } /// Reports whether no later lifecycle transition is permitted. #[must_use] pub const fn is_terminal(&self) -> bool { return matches!(self, Self::Completed(_) | Self::Cancelled | Self::Failed); } } /// Passive owner of one Job identity and its validated lifecycle state. #[derive(Eq, PartialEq)] pub struct JobLifecycle { id: crate::JobId, kind: crate::JobKindCode, state: crate::JobState, } impl JobLifecycle { /// Creates one lifecycle in [`JobState::Created`] state. #[must_use] pub const fn new(id: crate::JobId, kind: crate::JobKindCode) -> Self { return Self { id, kind, state: crate::JobState::Created }; } /// Returns the logical Job identity. #[must_use] pub const fn id(&self) -> &crate::JobId { return &self.id; } /// Returns the stable Job family code. #[must_use] pub const fn kind(&self) -> &crate::JobKindCode { return &self.kind; } /// Returns the current lifecycle state. #[must_use] pub const fn state(&self) -> crate::JobState { return self.state; } /// Transitions a newly created Job into its running state. pub fn start(&mut self) -> crate::Result<()> { return self.transition(crate::JobState::Running); } /// Records that a running Job observed cooperative cancellation intent. pub fn mark_cancelling(&mut self) -> crate::Result<()> { return self.transition(crate::JobState::Cancelling); } /// Completes a running or cancelling Job with its explicit normal outcome. pub fn complete(&mut self, completion: crate::JobCompletion) -> crate::Result<()> { return self.transition(crate::JobState::Completed(completion)); } /// Marks a created or cancelling Job as cooperatively cancelled. pub fn mark_cancelled(&mut self) -> crate::Result<()> { return self.transition(crate::JobState::Cancelled); } /// Marks a running or cancelling Job as failed. pub fn fail(&mut self) -> crate::Result<()> { return self.transition(crate::JobState::Failed); } fn transition(&mut self, target: crate::JobState) -> crate::Result<()> { if !allowed_transition(self.state, target) { return std::result::Result::Err(transition_error(self.state, target)); } self.state = target; return std::result::Result::Ok(()); } } impl std::fmt::Debug for JobLifecycle { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.debug_struct("JobLifecycle").field("id", &self.id).field("kind", &self.kind).field("state", &self.state).finish(); } } fn allowed_transition(source: crate::JobState, target: crate::JobState) -> bool { return matches!( (source, target), (crate::JobState::Created, crate::JobState::Running) | (crate::JobState::Created, crate::JobState::Cancelled) | (crate::JobState::Running, crate::JobState::Cancelling) | (crate::JobState::Running, crate::JobState::Completed(_)) | (crate::JobState::Running, crate::JobState::Failed) | (crate::JobState::Cancelling, crate::JobState::Completed(_)) | (crate::JobState::Cancelling, crate::JobState::Cancelled) | (crate::JobState::Cancelling, crate::JobState::Failed) ); } fn transition_error(source: crate::JobState, target: crate::JobState) -> crate::Error { return crate::Error::new(crate::ERROR_CODE_JOB_TRANSITION_INVALID, "invalid Job lifecycle transition") .with_context("source_state", source.code()) .with_context("target_state", target.code()); } #[cfg(test)] #[path = "../unit_tests/lifecycle.rs"] mod tests;