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