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
//! Shared graceful-shutdown signal helper for trusty-* daemons.
//!
//! Why: trusty-search, trusty-memory, and trusty-analyze all need to wait for
//! SIGTERM (launchd `bootout`, `kill <pid>`) or SIGINT (Ctrl-C in dev) before
//! cleanly draining in-flight HTTP requests. Centralising the implementation
//! removes three-way duplication and ensures every daemon responds identically
//! to the same signals.
//!
//! What: exposes a single async `shutdown_signal()` function that returns once
//! EITHER SIGTERM (unix) OR SIGINT/Ctrl-C (all platforms) fires. On non-unix
//! platforms only Ctrl-C is watched.
//!
//! Test: `cargo test -p trusty-common --features unconditional-only --
//! shutdown` runs the compilation smoke test. Signal delivery itself cannot be
//! triggered inside a unit test without `raise(SIGTERM)`, which is unsafe; the
//! integration tests in trusty-search exercise the full axum
//! `with_graceful_shutdown` path.
/// Seconds a trusty-* daemon is granted between SIGTERM and SIGKILL (#4393).
///
/// Why: this is the ONE number every terminator and every terminated daemon has
/// to agree on, and before #4393 nobody stated it. launchd's `ExitTimeOut`
/// default is documented only as "system-defined" and measures **5 s** on
/// macOS; `trusty-search stop` allowed 5 s; its orphan reaper allowed 3 s.
/// Meanwhile trusty-search's shutdown flush floors each index's budget at 30 s
/// (`service::shutdown_flush::MIN_FLUSH_TIMEOUT_SECS`), so no flush that had
/// real work to do could ever run to completion — it was SIGKILLed mid-write on
/// every path. Publishing the window as a shared constant is what lets the
/// plist renderer, the CLI stop path, the reaper, and the daemon's own flush
/// planner be checked against each other instead of drifting silently.
///
/// What: 60 s. Deliberately modest — long enough to clear the 30 s per-index
/// floor, short enough that a wedged daemon does not stall reboot or
/// `launchctl bootout` for minutes. Not derived from the flush budget's
/// 20-minute ceiling: a multi-minute `ExitTimeOut` in a static plist trades one
/// operator-visible failure for a worse one.
///
/// Test: `termination_grace_clears_the_measured_launchd_default`,
/// `render_plist_declares_exit_timeout`. trusty-search additionally asserts
/// this window covers its own per-index flush floor, in
/// `service::shutdown_budget`, `commands::stop`, and
/// `commands::start::reap_orphans`.
pub const TERMINATION_GRACE_SECS: u64 = 60;
/// Wall-clock time held back from the grace window for post-drain cleanup
/// (#4393, shared since #6601).
///
/// Why: [`TERMINATION_GRACE_SECS`] is the window that ends in SIGKILL, so a
/// component that spends ALL of it has left nothing for the work that runs
/// after it. Two components run that work today. `trusty-search` flushes each
/// index and then removes its port file, deregisters discovery and releases its
/// lockfile. `trusty-common`'s `uds::server::serve_until_idle` drains in-flight
/// connections and then hands control back to a caller that unlinks the socket —
/// and in `trusty-memory` runs the BM25 exit flush AFTER `serve_until`
/// (`transport::uds::serve_with_shutdown`), which `bm25_lane::shutdown`
/// documents as having "no window in which a SIGKILL can land mid-flush". A
/// drain defaulted to the whole grace window makes that claim false.
///
/// 🔴 **Holding time back is not the same as spending it well (#6601 review).**
/// This reserve bounds the drain; it cannot bound the cleanup itself, and
/// `bm25_lane::shutdown` had no deadline of its own — a slow flush spent the
/// reserve and the socket unlink after it never ran. So `trusty-memory`'s
/// `transport::uds::serve_with_shutdown` now awaits that flush UNDER this
/// duration (`flush_within_reserve`), which is what makes the sentence above
/// true rather than aspirational. A component that adds post-drain work owes the
/// same bound.
///
/// What: 5 s, subtracted from the grace window by every component that plans
/// inside it. One definition rather than two, per the common-entry-point rule —
/// `trusty-search`'s `service::shutdown_budget::CLEANUP_RESERVE` re-exports this.
///
/// Test: `default_serve_options_reserve_cleanup_time_inside_the_grace_window`,
/// trusty-memory's `an_exit_flush_that_overruns_the_reserve_is_abandoned`.
/// trusty-search's `shutdown_budget_tests.rs` covers the flush side.
pub const CLEANUP_RESERVE: Duration = from_secs;
/// The part of the grace window a component may actually plan to spend.
///
/// Why: every caller that subtracts [`CLEANUP_RESERVE`] by hand is a place the
/// saturation can be got wrong — a grace window shorter than the reserve must
/// yield an immediately-exhausted budget, never a wrapped enormous one.
/// What: [`termination_grace`] minus [`CLEANUP_RESERVE`], saturating at zero.
/// Test: `plannable_grace_reserves_cleanup_time`,
/// `plannable_grace_saturates_below_the_reserve`.
/// Pure half of [`plannable_grace`], over an already-resolved window.
///
/// Why: pure so the saturation is testable without touching the process
/// environment.
/// Test: `plannable_grace_saturates_below_the_reserve`.
/// Operator override for [`TERMINATION_GRACE_SECS`].
///
/// Why: a host whose launchd `ExitTimeOut` cannot be raised (a stale installed
/// plist, a container supervisor with its own `TimeoutStopSec`) needs to tell
/// the daemon the truth about its window, or the daemon plans a 55 s flush
/// inside a 5 s life and loses more than it would have with an accurate,
/// smaller budget. The env var is how the terminator declares the real number.
/// What: `TRUSTY_TERMINATION_GRACE_SECS`, a positive integer count of seconds.
/// Test: `termination_grace_honours_a_valid_override`,
/// `termination_grace_ignores_junk_overrides`.
pub const TERMINATION_GRACE_ENV: &str = "TRUSTY_TERMINATION_GRACE_SECS";
/// The termination window this process should plan for.
///
/// Why: reads the override at the one place that owns the policy so no caller
/// re-implements the parse. See [`TERMINATION_GRACE_SECS`] for why the number
/// exists at all.
/// What: [`termination_grace_from`] applied to [`TERMINATION_GRACE_ENV`].
/// Test: covered through `termination_grace_from`'s tests; this wrapper only
/// supplies the env read.
/// Pure half of [`termination_grace`], over an already-read env value.
///
/// Why: pure so the parse and its fallbacks are testable without mutating the
/// process environment (which races every other test in the binary).
/// What: a trimmed, positive integer wins; unset, empty, `0`, and unparseable
/// all fall back to [`TERMINATION_GRACE_SECS`]. A junk value must never shorten
/// the window to zero — a zero-length grace makes every flush a no-op.
/// Test: `termination_grace_honours_a_valid_override`,
/// `termination_grace_ignores_junk_overrides`.
/// Await SIGTERM (unix) or SIGINT/Ctrl-C (all platforms), whichever fires first.
///
/// Why: axum's `with_graceful_shutdown` takes an `async fn()` — it polls the
/// future and stops accepting new connections when it resolves. Passing
/// `shutdown_signal()` here lets every daemon drain in-flight requests before
/// the process exits, which is essential for connection-safe daemon upgrades
/// (issue #534). The shared helper guarantees trusty-search, trusty-memory, and
/// trusty-analyze all respond identically to `launchctl bootout` (SIGTERM).
///
/// What: on unix, registers handlers for both `SIGTERM` and `SIGINT` at
/// construction time and resolves when the first one fires. On non-unix
/// platforms (Windows), only Ctrl-C is watched. Signal registration errors
/// are downgraded to a warning; the function then falls back to watching
/// Ctrl-C only so the daemon still responds to interactive interrupts.
///
/// Test: compile with `cargo check -p trusty-common`; end-to-end coverage is
/// in `crates/trusty-search/tests/` which boots an axum daemon and sends SIGTERM.
pub async