Skip to main content

shep_core/protocol/request/
outcomes.rs

1//! Per-row outcome and reply pairs for the action, signal, and line verbs.
2
3use serde::{Deserialize, Serialize};
4
5// Named by intra-doc links and by nothing rustc compiles, so the
6// import is behind `cfg(doc)` rather than flagged unused.
7#[cfg(doc)]
8use super::ProcessInfo;
9
10/// What happened when the daemon tried to deliver one sheep's triggered
11/// action.
12// wire format: changing existing variants is a breaking change
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(tag = "kind", rename_all = "snake_case")]
15#[non_exhaustive]
16pub enum ActionOutcome {
17    /// The app answered on the shepherd channel.
18    Replied {
19        /// The reply body, exactly as the app sent it.
20        body: String,
21    },
22    /// The sheep had no reachable shepherd channel for the daemon to
23    /// deliver the action over.
24    NoChannel,
25    /// The sheep is a reload drainee, mid-swap and on its way out, so the
26    /// daemon skipped it rather than deliver the action to a process already
27    /// being replaced.
28    Skipped,
29    /// The daemon delivered the action, but no reply arrived before the
30    /// app's configured action timeout elapsed.
31    TimedOut,
32}
33
34/// One matched sheep's row in a `Trigger` reply.
35///
36/// Not a [`ProcessInfo`]: a reply body has nowhere to live on one.
37/// [`Self::outcome`] is per-row, since the selector grammar makes a mixed
38/// flock the normal case.
39// wire format: changing this is a breaking change
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ActionReply {
42    /// The sheep's stable id.
43    pub id: u32,
44    /// The sheep's name.
45    pub name: String,
46    /// What happened when the daemon tried to deliver the action.
47    pub outcome: ActionOutcome,
48}
49
50/// What happened when the shepherd tried to deliver one signal.
51// wire format: changing existing variants is a breaking change
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(tag = "kind", rename_all = "snake_case")]
54#[non_exhaustive]
55pub enum SignalOutcome {
56    /// The kernel accepted the signal for this sheep's pid.
57    ///
58    /// Says the signal was delivered, not that the app did anything with it.
59    /// A signal the app blocks or ignores is `Delivered` too.
60    Delivered,
61    /// The sheep is registered but has no live process to signal: stopped,
62    /// errored, or waiting out a restart backoff.
63    NotRunning,
64    /// The kernel refused the delivery; carries its reason (`ESRCH` for a
65    /// process reaped between the lookup and the syscall, `EPERM` for one this
66    /// daemon may not signal).
67    Failed {
68        /// The refusal, as the OS worded it.
69        reason: String,
70    },
71}
72
73/// One matched sheep's row in a `Signal` reply.
74///
75/// Per-row like [`ActionReply`]: the selector grammar makes a mixed flock
76/// the normal case.
77// wire format: changing this is a breaking change
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct SignalReply {
80    /// The sheep's stable id.
81    pub id: u32,
82    /// The sheep's name.
83    pub name: String,
84    /// What happened when the shepherd tried to deliver the signal.
85    pub outcome: SignalOutcome,
86}
87
88/// What happened when the shepherd tried to write one line to a sheep's stdin.
89// wire format: changing existing variants is a breaking change
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(tag = "kind", rename_all = "snake_case")]
92#[non_exhaustive]
93pub enum LineOutcome {
94    /// The line was written to the pipe and flushed.
95    ///
96    /// Says the bytes left the shepherd, not that the app read them. A pipe
97    /// holds 64 KiB before it blocks.
98    Sent,
99    /// The sheep has no stdin pipe: its config does not set `stdin = true`, or
100    /// it is not running.
101    ///
102    /// One outcome for two causes: both answer "there is no pipe here".
103    NoStdin,
104    /// The shepherd had a pipe and did not confirm a write to it; carries
105    /// why.
106    ///
107    /// Three shapes reach it: the write failed (the far end is gone), the
108    /// line found the sheep's queue already full, or the write did not
109    /// finish inside the shepherd's own bound. The reason names which.
110    ///
111    /// A timed-out write is not a promise the line was never written: the
112    /// bytes may be part-written into a pipe the app is not draining, and
113    /// land in full the moment it drains. A line still queued behind that one
114    /// is dropped once its caller gives up, so treat a retry as a second
115    /// command.
116    NotWritten {
117        /// What went wrong, in plain English.
118        reason: String,
119    },
120}
121
122/// One matched sheep's row in a `SendLine` reply.
123///
124/// Per-row like [`ActionReply`] and [`SignalReply`].
125// wire format: changing this is a breaking change
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct LineReply {
128    /// The sheep's stable id.
129    pub id: u32,
130    /// The sheep's name.
131    pub name: String,
132    /// What happened.
133    pub outcome: LineOutcome,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn action_outcome_kinds_serialize_snake_case_and_round_trip() {
142        // The shared snapshots exercise only `Replied`, the struct-shaped
143        // variant.
144        let cases = [
145            (
146                ActionOutcome::Replied {
147                    body: "pong".to_string(),
148                },
149                r#"{"kind":"replied","body":"pong"}"#,
150            ),
151            (ActionOutcome::NoChannel, r#"{"kind":"no_channel"}"#),
152            (ActionOutcome::Skipped, r#"{"kind":"skipped"}"#),
153            (ActionOutcome::TimedOut, r#"{"kind":"timed_out"}"#),
154        ];
155        for (outcome, wire) in cases {
156            assert_eq!(
157                serde_json::to_string(&outcome).unwrap(),
158                wire,
159                "{outcome:?}"
160            );
161            assert_eq!(
162                serde_json::from_str::<ActionOutcome>(wire).unwrap(),
163                outcome
164            );
165        }
166    }
167}