Skip to main content

trellis_core/
host_status.rs

1use crate::{ErrorCategory, ResourceKey, Revision, ScopeId};
2
3/// Host-observed resource outcome carried by a canonical status input.
4#[derive(Clone, Debug, Eq, PartialEq)]
5pub enum HostResourceOutcome {
6    /// The host has not reported a resource outcome.
7    Unknown,
8    /// The resource is live according to the host.
9    Open,
10    /// The resource failed outside graph propagation.
11    Failed(String),
12    /// The resource is closed according to the host.
13    Closed,
14    /// The host cannot apply the requested transition.
15    Unsupported(String),
16}
17
18impl HostResourceOutcome {
19    /// Returns the model category for host-reported resource status.
20    pub const fn category(&self) -> ErrorCategory {
21        ErrorCategory::HostResourceStatus
22    }
23}
24
25/// Canonical host report for a resource command observed outside the graph.
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct HostResourceStatus<S = HostResourceOutcome> {
28    /// Stable graph-visible resource identity.
29    pub resource_key: ResourceKey,
30    /// Scope associated with the command being reported.
31    pub scope: ScopeId,
32    /// Graph revision of the resource command the host is reporting for.
33    pub command_revision: Revision,
34    /// Monotonic host observation revision.
35    pub status_revision: Revision,
36    /// Application-defined status payload.
37    pub status: S,
38}
39
40impl<S> HostResourceStatus<S> {
41    /// Creates a host resource status input.
42    pub fn new(
43        resource_key: ResourceKey,
44        scope: ScopeId,
45        command_revision: Revision,
46        status_revision: Revision,
47        status: S,
48    ) -> Self {
49        Self {
50            resource_key,
51            scope,
52            command_revision,
53            status_revision,
54            status,
55        }
56    }
57
58    /// Returns the model category for host-reported resource status.
59    pub const fn category(&self) -> ErrorCategory {
60        ErrorCategory::HostResourceStatus
61    }
62
63    /// Maps the status payload while preserving structural identity.
64    pub fn map_status<T>(self, map: impl FnOnce(S) -> T) -> HostResourceStatus<T> {
65        HostResourceStatus {
66            resource_key: self.resource_key,
67            scope: self.scope,
68            command_revision: self.command_revision,
69            status_revision: self.status_revision,
70            status: map(self.status),
71        }
72    }
73}
74
75/// Classification for host status delivery relative to graph command state.
76#[derive(Copy, Clone, Debug, Eq, PartialEq)]
77pub enum HostStatusClass {
78    /// Status matches the current resource/scope/revision.
79    Current,
80    /// Status duplicates an already accepted status revision.
81    Duplicate,
82    /// Status targets an old command revision.
83    Stale,
84    /// Status targets a command revision newer than the graph has observed.
85    Future,
86    /// Status targets a resource or scope that is no longer current.
87    Late,
88}
89
90/// Graph command state needed to classify a host resource status.
91#[derive(Copy, Clone, Debug, Eq, PartialEq)]
92pub struct HostResourceCommandState {
93    /// Scope associated with the latest command for this resource.
94    pub scope: ScopeId,
95    /// Latest command revision known for this resource.
96    pub command_revision: Revision,
97    /// Whether the resource is still live in graph state.
98    pub resource_is_live: bool,
99    /// Whether the status scope currently owns the live resource.
100    pub scope_owns_resource: bool,
101}
102
103/// Classifies a host resource status against graph command state.
104pub fn classify_host_resource_status(
105    status: &HostResourceStatus,
106    state: Option<HostResourceCommandState>,
107    duplicate: bool,
108) -> HostStatusClass {
109    let Some(state) = state else {
110        return HostStatusClass::Late;
111    };
112
113    if !state.resource_is_live {
114        if matches!(status.status, HostResourceOutcome::Closed) && state.scope == status.scope {
115            return classify_revision(status.command_revision, state.command_revision, duplicate);
116        }
117        return HostStatusClass::Late;
118    }
119
120    if !state.scope_owns_resource {
121        return HostStatusClass::Late;
122    }
123
124    classify_revision(status.command_revision, state.command_revision, duplicate)
125}
126
127fn classify_revision(
128    status_revision: Revision,
129    command_revision: Revision,
130    duplicate: bool,
131) -> HostStatusClass {
132    if status_revision < command_revision {
133        HostStatusClass::Stale
134    } else if status_revision > command_revision {
135        HostStatusClass::Future
136    } else if duplicate {
137        HostStatusClass::Duplicate
138    } else {
139        HostStatusClass::Current
140    }
141}