Skip to main content

rskit_process/persistent/
error.rs

1use crate::{AppError, ErrorCode};
2
3const PERSISTENT_START_ERROR_KIND_DETAIL: &str = "rskit_process.persistent_start_error_kind";
4
5/// Machine-readable persistent process startup failure classification.
6#[derive(Debug, Clone, Copy, Eq, PartialEq)]
7#[non_exhaustive]
8pub enum PersistentStartErrorKind {
9    /// The persistent process could not be spawned.
10    SpawnFailed,
11    /// The readiness command timed out.
12    ReadinessCommandTimedOut,
13    /// The readiness command exited unsuccessfully.
14    ReadinessCommandFailed,
15    /// The process did not become ready before the readiness timeout.
16    ReadinessTimedOut,
17    /// Output streams ended before output readiness was observed.
18    OutputEndedBeforeReadiness,
19    /// The process exited before becoming ready.
20    ExitedBeforeReadiness,
21}
22
23impl PersistentStartErrorKind {
24    const fn as_str(self) -> &'static str {
25        match self {
26            Self::SpawnFailed => "spawn_failed",
27            Self::ReadinessCommandTimedOut => "readiness_command_timed_out",
28            Self::ReadinessCommandFailed => "readiness_command_failed",
29            Self::ReadinessTimedOut => "readiness_timed_out",
30            Self::OutputEndedBeforeReadiness => "output_ended_before_readiness",
31            Self::ExitedBeforeReadiness => "exited_before_readiness",
32        }
33    }
34
35    fn from_str(value: &str) -> Option<Self> {
36        match value {
37            "spawn_failed" => Some(Self::SpawnFailed),
38            "readiness_command_timed_out" => Some(Self::ReadinessCommandTimedOut),
39            "readiness_command_failed" => Some(Self::ReadinessCommandFailed),
40            "readiness_timed_out" => Some(Self::ReadinessTimedOut),
41            "output_ended_before_readiness" => Some(Self::OutputEndedBeforeReadiness),
42            "exited_before_readiness" => Some(Self::ExitedBeforeReadiness),
43            _ => None,
44        }
45    }
46}
47
48/// Return the structured persistent startup error kind attached to an [`AppError`].
49pub fn persistent_start_error_kind(error: &AppError) -> Option<PersistentStartErrorKind> {
50    error
51        .details()
52        .get(PERSISTENT_START_ERROR_KIND_DETAIL)
53        .and_then(|value| value.as_str())
54        .and_then(PersistentStartErrorKind::from_str)
55}
56
57pub(in crate::persistent) fn persistent_start_error(
58    kind: PersistentStartErrorKind,
59    code: ErrorCode,
60    message: impl Into<String>,
61) -> AppError {
62    AppError::new(code, message).with_detail(PERSISTENT_START_ERROR_KIND_DETAIL, kind.as_str())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn persistent_start_error_round_trips_all_structured_kinds() {
71        for kind in [
72            PersistentStartErrorKind::SpawnFailed,
73            PersistentStartErrorKind::ReadinessCommandTimedOut,
74            PersistentStartErrorKind::ReadinessCommandFailed,
75            PersistentStartErrorKind::ReadinessTimedOut,
76            PersistentStartErrorKind::OutputEndedBeforeReadiness,
77            PersistentStartErrorKind::ExitedBeforeReadiness,
78        ] {
79            let error = persistent_start_error(kind, ErrorCode::Internal, "failed");
80            assert_eq!(persistent_start_error_kind(&error), Some(kind));
81        }
82    }
83
84    #[test]
85    fn missing_or_unknown_persistent_error_kind_returns_none() {
86        assert_eq!(
87            persistent_start_error_kind(&AppError::new(ErrorCode::Internal, "plain")),
88            None
89        );
90        let error = AppError::new(ErrorCode::Internal, "bad")
91            .with_detail(PERSISTENT_START_ERROR_KIND_DETAIL, "unknown");
92        assert_eq!(persistent_start_error_kind(&error), None);
93    }
94}