1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
//! Per-service configuration for [`super::UdsServiceSupervisor`] (#5089).
//!
//! Why: the supervisor this generalises carried two timing constants bound to
//! `trusty-bm25-daemon` specifically. `SPAWN_PROBE_TIMEOUT` was 3 s, justified
//! in its own doc comment by "BM25 has no model-loading step" against the
//! embedder's 30 s. `SIGTERM_PATIENCE_SECS` was 5, tied to the daemon's real
//! flush budget by a `const _: () = assert!(SIGTERM_PATIENCE_SECS >
//! trusty_bm25_daemon::SHUTDOWN_FLUSH_TIMEOUT.as_secs(), …)`. Carried across as
//! bare constants both break silently for the first service with a model to
//! load or a longer flush, and each failure wears a misleading costume — a
//! spawn timeout that looks like a broken binary, and a SIGKILL landing inside
//! a flush that discards acked writes with no error anywhere. So they are
//! per-service values here.
//!
//! What: [`ServiceTimeouts`] carries the three numbers that must move together
//! and re-derives the compile-time guard as a precondition of a `const fn`
//! constructor — see [`ServiceTimeouts::new`]. [`SupervisorConfig`] carries the
//! population limits, the log label, and the external-mode opt-out.
//! [`SpawnSpec`] is what the supervisor executes, resolved lazily so a service
//! that is already running never pays for locating its binary.
//!
//! Test: `tests.rs` — `service_timeouts_reject_patience_equal_to_the_flush`,
//! `service_timeouts_carry_probe_defaults_and_honour_overrides`,
//! `supervisor_config_clamps_max_live_to_one`.
use OsString;
use PathBuf;
use Duration;
use SupervisorError;
/// Initial socket-probe interval, doubled on each miss.
///
/// 20 ms gives sub-50 ms detection on a fast bind without busy-waiting.
pub const DEFAULT_INITIAL_PROBE_INTERVAL: Duration = from_millis;
/// Ceiling on the exponential probe backoff.
pub const DEFAULT_MAX_PROBE_INTERVAL: Duration = from_millis;
/// Per-attempt timeout on the liveness connect.
///
/// Short on purpose: an unresponsive-but-bound socket must not stall the probe
/// loop, and a connect that does not settle inside it is
/// [`super::SocketVerdict::Inconclusive`] rather than a reason to kill anything.
pub const DEFAULT_CONNECT_PROBE_TIMEOUT: Duration = from_millis;
/// Const-evaluable `a > b` for [`Duration`].
///
/// Why: [`ServiceTimeouts::new`] must compare two durations inside a `const fn`,
/// and this pair of accessors has been `const` since long before this crate's
/// MSRV — unlike the whole-value comparison operators, which are not `const`.
const
/// The timing budget of ONE supervised service.
///
/// Why: see the module docs. Every field here is a statement about the
/// supervised child, not about supervision in general.
///
/// What: `spawn_probe` is how long [`super::UdsServiceSupervisor::ensure_running`]
/// waits for a freshly-spawned child to bind and accept. `shutdown_flush` is the
/// child's OWN shutdown budget, declared by the service so the relationship
/// below is checkable. `sigterm_patience` is how long the supervisor waits after
/// SIGTERM before escalating to SIGKILL, and it must strictly exceed
/// `shutdown_flush` — the child still needs signal delivery, the flush itself,
/// socket cleanup and exit inside that window.
///
/// 🔴 **Sourcing rule for `shutdown_flush`, and what the assert cannot check.**
/// `shutdown_flush` MUST be the supervised binary's own flush constant,
/// imported — `trusty_bm25_daemon::SHUTDOWN_FLUSH_TIMEOUT`, not a literal `2 s`
/// that happens to match it today. [`ServiceTimeouts::new`] enforces the
/// RELATION (`sigterm_patience > shutdown_flush`) and nothing else: it has no
/// way to know what your child's real budget is, so a literal that understates
/// it compiles, passes the assert, and SIGKILLs mid-flush — the exact failure
/// this type exists to prevent. A hardcoded copy also stays equal to itself
/// while the daemon's real value drifts, so it can never detect the drift.
/// Import the constant, or add a test pinning your value against it the way
/// `sigterm_patience_exceeds_the_daemon_flush_budget` does. If your service's
/// flush budget is not a constant you can name, that is the thing to fix first.
///
/// `#[non_exhaustive]`: free while `trusty-common` sits unpublished at 0.30.0
/// against a published 0.28.1, and never free again. On a STRUCT the attribute
/// bars construction from outside the crate, including functional-update syntax
/// (E0639) — which is the intent: the constructors are the only way in, and
/// they are where the guard lives. Use [`ServiceTimeouts::new`] for a `const`
/// declaration (compile-time check) and [`ServiceTimeouts::try_new`] when the
/// values are computed at runtime.
///
/// Test: `service_timeouts_reject_patience_equal_to_the_flush`,
/// `service_timeouts_carry_probe_defaults_and_honour_overrides`,
/// `try_new_rejects_an_inverted_pair_without_panicking`.
/// Everything a [`super::UdsServiceSupervisor`] needs that is fixed for its
/// lifetime.
///
/// Why: resolving the limits once at construction — rather than on each
/// `ensure_running` — means a mid-flight environment mutation cannot make the
/// cap wobble between two concurrent calls.
///
/// What: `service` is the label every log line carries. `max_live` caps
/// concurrently-live children, with least-recently-used reaping.
/// `rss_limit_mb` is a per-child ceiling compared against a real measurement;
/// `None` disables enforcement, which is a different instruction from a ceiling
/// of zero and so cannot share an encoding with it. `external_env` names an
/// environment variable that, when set to exactly `"1"`, suppresses spawning
/// entirely so an operator running the target under `tctl` (ADR-0011,
/// ADR-0034 §1) keeps ownership of its lifecycle.
///
/// Test: `supervisor_config_clamps_max_live_to_one`,
/// `external_env_only_honours_exactly_one`.
/// The command that starts one instance of a supervised service.
///
/// Why: resolved by a closure the supervisor calls only when it has decided to
/// spawn, so a service that is already running — or externally managed, or
/// adoptable from a socket someone else bound — never pays for locating its
/// binary, and a missing binary is not an error on any of those paths.
///
/// What: the program, its arguments, and directories to create first. `stdin`
/// and `stdout` are closed and `stderr` is inherited so the child's tracing
/// output reaches the parent's log stream; `kill_on_drop` is always set so an
/// unsupervised drop still reaps the child rather than leaking it.
///
/// Test: `spawn_spec_builder_accumulates_args_and_dirs`.