Skip to main content

shep_core/config/
apply.rs

1//! How each `AppConfig` field reaches a running sheep.
2//!
3//! Four answers, and the difference between them is where the daemon reads
4//! the field, not what the field means. A value read fresh at each decision
5//! can be swapped under a running process with no disruption; one baked
6//! into the child at exec cannot change until that process is replaced.
7
8use serde::{Deserialize, Serialize};
9
10/// Where a field's new value takes effect.
11///
12/// `#[non_exhaustive]`: a field could someday be applied by nudging the
13/// running child through `shep reopen`'s SIGUSR2 path, a fifth group
14/// distinct from the four below. shep-core is published, so an
15/// out-of-tree match on this enum needs a wildcard arm to survive that.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ApplyGroup {
19    /// Read fresh at each decision, so a write to the stored spec is enough.
20    Live,
21    /// Read when a process spawns, so a write reaches the next one.
22    NextSpawn,
23    /// Held by the running child, so that instance must be replaced.
24    NeedsRespawn,
25    /// Identity or flock shape, not a runtime knob.
26    Structural,
27}
28
29/// Every `AppConfig` field name this table has been taught about, paired with
30/// its group.
31///
32/// `apply_group` and `is_classified` both read this single list rather than
33/// keeping two hand-maintained lists in step: a field named here only once
34/// cannot drift between "classified" and "has a group".
35const FIELDS: &[(&str, ApplyGroup)] = &[
36    // Read by `brain::decide` when a sheep exits.
37    ("autorestart", ApplyGroup::Live),
38    ("max_restarts", ApplyGroup::Live),
39    ("min_uptime", ApplyGroup::Live),
40    ("restart_delay", ApplyGroup::Live),
41    ("exp_backoff_restart_delay", ApplyGroup::Live),
42    ("stop_exit_codes", ApplyGroup::Live),
43    // Read by `claim_manual` when a kill ladder runs.
44    ("kill_timeout", ApplyGroup::Live),
45    ("graceful_timeout", ApplyGroup::Live),
46    // Read fresh when extras arms a worker. These need a re-arm to take
47    // effect; see `ExtrasRegistry::rearm_name`.
48    ("max_memory", ApplyGroup::Live),
49    ("watch", ApplyGroup::Live),
50    ("ignore_watch", ApplyGroup::Live),
51    ("watch_delay", ApplyGroup::Live),
52    ("watch_options", ApplyGroup::Live),
53    ("cron_restart", ApplyGroup::Live),
54    ("cron_timezone", ApplyGroup::Live),
55    ("liveness_probe", ApplyGroup::Live),
56    // Read fresh per command.
57    ("fold", ApplyGroup::Live),
58    ("reuse_port", ApplyGroup::Live),
59    // Read fresh from the stored spec each time an action is dispatched, at
60    // `supervisor.rs`'s `begin_action` (`config.action_timeout.as_duration()`),
61    // not baked into the long-lived per-sheep task.
62    ("action_timeout", ApplyGroup::Live),
63    // Unlike its two ladder-mates above, `kill_signal` is read from the
64    // per-sheep task's `ResolvedApp`, moved in once at `spawn_sheep_task`
65    // and never refreshed.
66    ("kill_signal", ApplyGroup::NextSpawn),
67    ("listen_timeout", ApplyGroup::NextSpawn),
68    ("readiness_probe", ApplyGroup::NextSpawn),
69    // Read once at muster or boot, by `restorable()`.
70    ("autostart", ApplyGroup::NextSpawn),
71    // Baked into the child at exec: argv, cwd, environment, credentials, the
72    // fd table, the log paths it is already writing to.
73    ("script", ApplyGroup::NeedsRespawn),
74    ("args", ApplyGroup::NeedsRespawn),
75    ("cwd", ApplyGroup::NeedsRespawn),
76    ("interpreter", ApplyGroup::NeedsRespawn),
77    ("env", ApplyGroup::NeedsRespawn),
78    ("user", ApplyGroup::NeedsRespawn),
79    ("group", ApplyGroup::NeedsRespawn),
80    ("out_file", ApplyGroup::NeedsRespawn),
81    ("err_file", ApplyGroup::NeedsRespawn),
82    ("merge_logs", ApplyGroup::NeedsRespawn),
83    ("channel", ApplyGroup::NeedsRespawn),
84    ("stdin", ApplyGroup::NeedsRespawn),
85    ("wait_ready", ApplyGroup::NeedsRespawn),
86    // `shutdown_with_message` belongs here rather than with the kill ladder:
87    // `assemble()` ORs it into whether fd 3 is opened, and that is the
88    // child's own fd table.
89    ("shutdown_with_message", ApplyGroup::NeedsRespawn),
90    ("name", ApplyGroup::Structural),
91    ("instances", ApplyGroup::Structural),
92    // Read only by `normalize` to refuse it by name.
93    ("increment_var", ApplyGroup::Structural),
94];
95
96/// The group `field` belongs to.
97///
98/// An unknown name answers [`ApplyGroup::NeedsRespawn`], the most
99/// conservative of the four: a field this table has not been taught about
100/// gets a restart rather than a silent claim that it applied.
101/// `every_appconfig_field_has_a_group` keeps that arm unreachable for real
102/// fields.
103#[must_use]
104pub fn apply_group(field: &str) -> ApplyGroup {
105    FIELDS
106        .iter()
107        .find(|(name, _)| *name == field)
108        .map_or(ApplyGroup::NeedsRespawn, |(_, group)| *group)
109}
110
111/// Whether `field` is named explicitly in the table above, as opposed to
112/// reaching the conservative fallback. Test-facing.
113#[must_use]
114pub fn is_classified(field: &str) -> bool {
115    FIELDS.iter().any(|(name, _)| *name == field)
116}
117
118/// How much of a Flockfile load overwrites what the operator has set since.
119///
120/// Two independent axes: whether `env` is reset, and whether a key the
121/// template declares (or does not) is reset. Five of the six combinations
122/// are named here; each variant's own doc states its column values.
123///
124/// `#[non_exhaustive]` buys source compatibility only, forcing an
125/// out-of-tree match to carry a wildcard arm. It does nothing for serde:
126/// this enum has no `#[serde(other)]`, so an older build fails to
127/// deserialize a variant it predates with `unknown variant`.
128#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case")]
130#[non_exhaustive]
131pub enum ResetDepth {
132    /// Append keys nobody established. Overwrite nothing. The default,
133    /// because a Flockfile arrives from the app's own repository.
134    ///
135    /// `env`: kept. A key the template declares: kept, unless nobody has
136    /// established it yet, which is the append. A key it does not declare:
137    /// kept, since there is nothing to append it against.
138    #[default]
139    None,
140    /// Put back what the template declares, and nothing else.
141    ///
142    /// `env`: kept. A key the template declares: reset. A key it does not
143    /// declare: kept, so an app stocked to four instances against a file
144    /// with no `instances` line keeps its count.
145    File,
146    /// Put non-`env` settings back to the template, `env` kept. Every
147    /// setting goes back, declared or not, to the value a fresh start off
148    /// the template would give it: `env` is operator data, the rest is
149    /// operator-tuned policy, and resetting policy is recoverable while
150    /// resetting data is not.
151    ///
152    /// `env`: kept. A key the template declares: reset. A key it does not
153    /// declare: reset too, to the template's own default.
154    Policy,
155    /// Reset `env` back to the template and leave everything else alone.
156    ///
157    /// Touches data, not policy: on the settings axis this behaves like
158    /// `None`, append included, since it widens a load rather than
159    /// narrowing one.
160    ///
161    /// `env`: reset. A key the template declares: kept, save for the same
162    /// append `None` does. A key it does not declare: kept.
163    Env,
164    /// Put everything back to the template, `env` included, and drop the
165    /// override record.
166    ///
167    /// `env`: reset. A key the template declares: reset. A key it does not
168    /// declare: reset too, to the template's own default.
169    All,
170}
171
172#[cfg(test)]
173mod tests {
174    use super::{ApplyGroup, apply_group, is_classified};
175    use crate::config::AppConfig;
176
177    /// fails if any AppConfig field is missing from the table. A field added
178    /// to the struct without a group would route as its default and either
179    /// apply live when it cannot, or need a restart when it does not.
180    #[test]
181    fn every_appconfig_field_has_a_group() {
182        let serde_json::Value::Object(fields) = serde_json::to_value(AppConfig::default()).unwrap()
183        else {
184            panic!("AppConfig must serialize as an object");
185        };
186        let missing: Vec<&String> = fields.keys().filter(|k| !is_classified(k)).collect();
187        assert!(
188            missing.is_empty(),
189            "unclassified AppConfig fields: {missing:?}"
190        );
191    }
192
193    #[test]
194    fn kill_signal_reaches_the_next_spawn_not_the_next_kill() {
195        assert_eq!(apply_group("kill_signal"), ApplyGroup::NextSpawn);
196        assert_eq!(apply_group("kill_timeout"), ApplyGroup::Live);
197        assert_eq!(apply_group("graceful_timeout"), ApplyGroup::Live);
198    }
199
200    #[test]
201    fn shutdown_with_message_is_baked_into_the_child() {
202        assert_eq!(
203            apply_group("shutdown_with_message"),
204            ApplyGroup::NeedsRespawn
205        );
206    }
207
208    /// fails if the split drifts from what the spec recorded.
209    #[test]
210    fn the_split_is_nineteen_four_fourteen_three() {
211        let serde_json::Value::Object(fields) = serde_json::to_value(AppConfig::default()).unwrap()
212        else {
213            panic!("AppConfig must serialize as an object");
214        };
215        let count = |want: ApplyGroup| fields.keys().filter(|k| apply_group(k) == want).count();
216        assert_eq!(count(ApplyGroup::Live), 19, "Live");
217        assert_eq!(count(ApplyGroup::NextSpawn), 4, "NextSpawn");
218        assert_eq!(count(ApplyGroup::NeedsRespawn), 14, "NeedsRespawn");
219        assert_eq!(count(ApplyGroup::Structural), 3, "Structural");
220    }
221}