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    // Read once when a batch is ordered, at a boot, a muster, or a staged
72    // start; see the field's own doc comment on `AppConfig`.
73    ("depends_on", ApplyGroup::NextSpawn),
74    // Baked into the child at exec: argv, cwd, environment, credentials, the
75    // fd table, the log paths it is already writing to.
76    ("script", ApplyGroup::NeedsRespawn),
77    ("args", ApplyGroup::NeedsRespawn),
78    ("cwd", ApplyGroup::NeedsRespawn),
79    ("interpreter", ApplyGroup::NeedsRespawn),
80    ("env", ApplyGroup::NeedsRespawn),
81    // Decides what every `{{secret:...}}` in this child's env resolved to,
82    // and those are baked in at exec like the rest of the environment.
83    ("environment", ApplyGroup::NeedsRespawn),
84    ("user", ApplyGroup::NeedsRespawn),
85    ("group", ApplyGroup::NeedsRespawn),
86    ("out_file", ApplyGroup::NeedsRespawn),
87    ("err_file", ApplyGroup::NeedsRespawn),
88    ("merge_logs", ApplyGroup::NeedsRespawn),
89    ("channel", ApplyGroup::NeedsRespawn),
90    ("stdin", ApplyGroup::NeedsRespawn),
91    ("wait_ready", ApplyGroup::NeedsRespawn),
92    // `shutdown_with_message` belongs here rather than with the kill ladder:
93    // `assemble()` ORs it into whether fd 3 is opened, and that is the
94    // child's own fd table.
95    ("shutdown_with_message", ApplyGroup::NeedsRespawn),
96    ("name", ApplyGroup::Structural),
97    ("instances", ApplyGroup::Structural),
98    // Read only by `normalize` to refuse it by name.
99    ("increment_var", ApplyGroup::Structural),
100];
101
102/// The group `field` belongs to.
103///
104/// An unknown name answers [`ApplyGroup::NeedsRespawn`], the most
105/// conservative of the four: a field this table has not been taught about
106/// gets a restart rather than a silent claim that it applied.
107/// `every_appconfig_field_has_a_group` keeps that arm unreachable for real
108/// fields.
109#[must_use]
110pub fn apply_group(field: &str) -> ApplyGroup {
111    FIELDS
112        .iter()
113        .find(|(name, _)| *name == field)
114        .map_or(ApplyGroup::NeedsRespawn, |(_, group)| *group)
115}
116
117/// Whether `field` is named explicitly in the table above, as opposed to
118/// reaching the conservative fallback. Test-facing.
119#[must_use]
120pub fn is_classified(field: &str) -> bool {
121    FIELDS.iter().any(|(name, _)| *name == field)
122}
123
124/// How much of a Flockfile load overwrites what the operator has set since.
125///
126/// Two independent axes: whether `env` is reset, and whether a key the
127/// template declares (or does not) is reset. Five of the six combinations
128/// are named here; each variant's own doc states its column values.
129///
130/// `#[non_exhaustive]` buys source compatibility only, forcing an
131/// out-of-tree match to carry a wildcard arm. It does nothing for serde:
132/// this enum has no `#[serde(other)]`, so an older build fails to
133/// deserialize a variant it predates with `unknown variant`.
134#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136#[non_exhaustive]
137pub enum ResetDepth {
138    /// Append keys nobody established. Overwrite nothing. The default,
139    /// because a Flockfile arrives from the app's own repository.
140    ///
141    /// `env`: kept. A key the template declares: kept, unless nobody has
142    /// established it yet, which is the append. A key it does not declare:
143    /// kept, since there is nothing to append it against.
144    #[default]
145    None,
146    /// Put back what the template declares, and nothing else.
147    ///
148    /// `env`: kept. A key the template declares: reset. A key it does not
149    /// declare: kept, so an app stocked to four instances against a file
150    /// with no `instances` line keeps its count.
151    File,
152    /// Put non-`env` settings back to the template, `env` kept. Every
153    /// setting goes back, declared or not, to the value a fresh start off
154    /// the template would give it: `env` is operator data, the rest is
155    /// operator-tuned policy, and resetting policy is recoverable while
156    /// resetting data is not.
157    ///
158    /// `env`: kept. A key the template declares: reset. A key it does not
159    /// declare: reset too, to the template's own default.
160    Policy,
161    /// Reset `env` back to the template and leave everything else alone.
162    ///
163    /// Touches data, not policy: on the settings axis this behaves like
164    /// `None`, append included, since it widens a load rather than
165    /// narrowing one.
166    ///
167    /// `env`: reset. A key the template declares: kept, save for the same
168    /// append `None` does. A key it does not declare: kept.
169    Env,
170    /// Put everything back to the template, `env` included, and drop the
171    /// override record.
172    ///
173    /// `env`: reset. A key the template declares: reset. A key it does not
174    /// declare: reset too, to the template's own default.
175    All,
176}
177
178#[cfg(test)]
179mod tests {
180    use super::{ApplyGroup, apply_group, is_classified};
181    use crate::config::AppConfig;
182
183    /// fails if any AppConfig field is missing from the table. A field added
184    /// to the struct without a group would route as its default and either
185    /// apply live when it cannot, or need a restart when it does not.
186    #[test]
187    fn every_appconfig_field_has_a_group() {
188        let serde_json::Value::Object(fields) = serde_json::to_value(AppConfig::default()).unwrap()
189        else {
190            panic!("AppConfig must serialize as an object");
191        };
192        let missing: Vec<&String> = fields.keys().filter(|k| !is_classified(k)).collect();
193        assert!(
194            missing.is_empty(),
195            "unclassified AppConfig fields: {missing:?}"
196        );
197    }
198
199    #[test]
200    fn kill_signal_reaches_the_next_spawn_not_the_next_kill() {
201        assert_eq!(apply_group("kill_signal"), ApplyGroup::NextSpawn);
202        assert_eq!(apply_group("kill_timeout"), ApplyGroup::Live);
203        assert_eq!(apply_group("graceful_timeout"), ApplyGroup::Live);
204    }
205
206    #[test]
207    fn shutdown_with_message_is_baked_into_the_child() {
208        assert_eq!(
209            apply_group("shutdown_with_message"),
210            ApplyGroup::NeedsRespawn
211        );
212    }
213
214    #[test]
215    fn depends_on_applies_at_the_next_spawn() {
216        // fails if the field is classified Live, which would claim an edit
217        // reaches a running flock's order, or Structural, which would route it
218        // through handle_scale
219        assert_eq!(apply_group("depends_on"), ApplyGroup::NextSpawn);
220        assert!(is_classified("depends_on"));
221    }
222
223    /// fails if the split drifts from what the spec recorded.
224    #[test]
225    fn the_split_is_nineteen_five_fifteen_three() {
226        let serde_json::Value::Object(fields) = serde_json::to_value(AppConfig::default()).unwrap()
227        else {
228            panic!("AppConfig must serialize as an object");
229        };
230        let count = |want: ApplyGroup| fields.keys().filter(|k| apply_group(k) == want).count();
231        assert_eq!(count(ApplyGroup::Live), 19, "Live");
232        assert_eq!(count(ApplyGroup::NextSpawn), 5, "NextSpawn");
233        assert_eq!(count(ApplyGroup::NeedsRespawn), 15, "NeedsRespawn");
234        assert_eq!(count(ApplyGroup::Structural), 3, "Structural");
235    }
236}