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}