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
//! Per-palace `trusty-bm25-daemon` spawn supervisor (issue #193).
//!
//! Why: trusty-memory ships the `trusty-bm25-daemon` binary alongside its own
//! but never actually spawned it, so operators who set `TRUSTY_BM25_DAEMON=1`
//! had to babysit one daemon per palace by hand. This module makes BM25 a
//! single-process concern: on first BM25 use for a palace, discover the binary,
//! spawn a child with the right `--palace` + `--data-dir`, poll the socket until
//! it serves, and own the child for the rest of the daemon's life.
//!
//! What: a thin BM25-shaped face over
//! [`trusty_common::uds::supervisor::UdsServiceSupervisor`] (#5089 step 2). The
//! state machine — spawn-gate serialisation, socket adoption, the LRU cap, the
//! RSS ceiling, socket-backed liveness, the doomed queue, SIGTERM→SIGKILL — is
//! shared now, so `trusty-console` inherits the same hardening for
//! `trusty-review` / `trusty-analyze` (ADR-0034 §1) instead of re-earning it.
//! What stays here is what is genuinely BM25's: the env-var knobs, the socket
//! path convention, the daemon's argv, and — see [`BM25_TIMEOUTS`] — the two
//! timing numbers that are statements about `trusty-bm25-daemon` rather than
//! about supervision.
//!
//! Test: unit tests in `bm25_supervisor_tests.rs` cover the env knobs, the
//! external-mode opt-out, socket adoption, idempotent shutdown, and the
//! patience-vs-flush relationship. `tests/bm25_supervisor_concurrency.rs` drives
//! real daemon children through the double-spawn, aggregate-cap, dead-child,
//! unserved-socket and evicted-live-child-flush paths. The shared machinery's
//! own unit coverage lives in `trusty-common`'s `uds/supervisor/tests.rs`.
use ;
use Duration;
use ;
use ;
use ;
/// Environment variable that disables spawn supervision entirely.
///
/// Why: operators who manage `trusty-bm25-daemon` themselves (launchd plist,
/// systemd unit, docker sidecar) must be able to opt the in-process supervisor
/// out so two daemons never fight over one socket.
/// What: `TRUSTY_BM25_EXTERNAL`. Any value other than `"1"` is treated as unset.
/// Test: `external_mode_skips_spawn`.
pub const ENV_EXTERNAL_BM25: &str = "TRUSTY_BM25_EXTERNAL";
/// Environment variable that overrides the cap on concurrently-live daemons.
///
/// Why (#2845): `ensure_running` is called once per palace, and one
/// `memory_recall_all` touches every palace on disk — ~99 on this host. Without
/// a cap the supervisor would hold 99 child processes for the rest of the
/// trusty-memory daemon's life. Drawer distribution is heavily skewed, so the
/// working set is a handful of palaces and a small cap costs almost nothing.
/// What: `TRUSTY_BM25_MAX_DAEMONS`, parsed as `usize`; values below 1 and
/// unparseable values fall back to [`DEFAULT_MAX_LIVE_DAEMONS`].
/// Test: `max_live_daemons_honours_env_override`.
pub const ENV_MAX_DAEMONS: &str = "TRUSTY_BM25_MAX_DAEMONS";
/// Environment variable that overrides the per-daemon RSS ceiling, in MB.
///
/// Why (#2846): trusty-search declared an `rss_limit_mb` and never compared it
/// against anything; the process grew to 2.2x that limit and was OOM-killed. A
/// BM25 daemon's memory scales linearly with its palace's drawer text because
/// `PalaceBm25Index` retains every document's full text, so an unbounded palace
/// is an unbounded daemon.
/// What: `TRUSTY_BM25_RSS_LIMIT_MB`. `0` disables enforcement; unparseable
/// values fall back to [`DEFAULT_RSS_LIMIT_MB`].
/// Test: `rss_limit_honours_env_override`.
pub const ENV_RSS_LIMIT_MB: &str = "TRUSTY_BM25_RSS_LIMIT_MB";
/// Default cap on concurrently-live BM25 daemons.
///
/// Why: three covers the realistic working set — the palace you are in, the one
/// you just cross-referenced, and one in flight — while keeping the worst case
/// at three subprocesses instead of ninety-nine.
/// What: `3`.
/// Test: `default_cap_is_three`.
pub const DEFAULT_MAX_LIVE_DAEMONS: usize = 3;
/// Default per-daemon RSS ceiling in megabytes.
///
/// Why: the whole drawer corpus across ~99 palaces is single-digit MB of text,
/// so a single daemon holding more than 512 MB is not holding drawers — it is
/// leaking, and #2846 is the record of what happens when nobody notices.
/// What: `512`.
/// Test: `rss_limit_honours_env_override`.
pub const DEFAULT_RSS_LIMIT_MB: u64 = 512;
/// Upper bound on how long `ensure_running` waits for a freshly-spawned daemon
/// to bind and accept.
///
/// Why: BM25's bind step is fast — the snapshot load is the slowest part and
/// runs on a tempdir-sized fixture in tests — so 3 s is comfortably more than
/// the observed worst case while still failing fast on a misconfigured spawn.
/// The 10x gap against the embedder supervisor's 30 s default is entirely
/// explained by BM25 having no model to load, which is exactly why this number
/// is declared here rather than in the shared supervisor.
const SPAWN_PROBE_TIMEOUT: Duration = from_millis;
/// How long the supervisor waits after SIGTERM before escalating to SIGKILL.
///
/// Why: strictly greater than the daemon's own `SHUTDOWN_FLUSH_TIMEOUT` (2 s),
/// because the daemon needs signal delivery, the flush itself, socket cleanup
/// and exit inside this window. At an equal budget the SIGKILL lands mid-flush
/// and the open write window is lost.
/// Test: `sigterm_patience_exceeds_the_daemon_flush_budget`.
const SIGTERM_PATIENCE: Duration = from_secs;
/// `trusty-bm25-daemon`'s timing budget, and the compile-time guard on it.
///
/// 🔴 This `const` item is what replaces the old
/// `const _: () = assert!(SIGTERM_PATIENCE_SECS > …SHUTDOWN_FLUSH_TIMEOUT…)`
/// (#5085). That assertion could not cross into `trusty-common` — a shared
/// supervisor cannot name any particular daemon's flush budget without depending
/// on it — so the check moved into [`ServiceTimeouts::new`], which is a
/// `const fn`. Evaluating it in a `const` item runs the assert at compile time,
/// exactly as before, and binds it to the value actually handed to the
/// supervisor rather than to a free-standing constant a refactor could leave
/// behind. Lower `SIGTERM_PATIENCE` to 2 s, or raise the daemon's
/// `SHUTDOWN_FLUSH_TIMEOUT` past it, and this line fails the build.
///
/// The daemon's budget is imported, never restated: a hardcoded copy stays equal
/// to itself while the real value drifts, so it could not detect the drift it
/// exists to name.
/// Test: `sigterm_patience_exceeds_the_daemon_flush_budget` pins the margin;
/// the strict inequality is the compiler's job.
const BM25_TIMEOUTS: ServiceTimeouts = new;
/// Supervisor that owns BM25 daemon subprocesses, one per palace.
///
/// Why: trusty-memory wants the BM25 lane to be zero-touch — set
/// `TRUSTY_BM25_DAEMON=1` and recall just gets a lexical boost. Owning the
/// children here means the trusty-memory daemon's lifetime IS the BM25 daemons'
/// lifetime.
/// What: a [`UdsServiceSupervisor`] keyed by palace id, plus BM25's socket-path
/// and argv conventions. Every method is `&self` so the supervisor can live
/// behind an `Arc`.
/// Test: `bm25_supervisor_tests.rs` and `tests/bm25_supervisor_concurrency.rs`.
/// Resolve the live-daemon cap from [`ENV_MAX_DAEMONS`].
///
/// Why: an operator who fans out wider than the default working set needs a
/// knob, and a knob that silently ignores a typo is worse than no knob.
/// What: parses as `usize`; `0` and unparseable values fall back to
/// [`DEFAULT_MAX_LIVE_DAEMONS`].
/// Test: `max_live_daemons_honours_env_override`.
/// Resolve the per-daemon RSS ceiling from [`ENV_RSS_LIMIT_MB`].
///
/// Why: same knob-with-a-typo argument as [`max_live_from_env`], plus one
/// specific to this limit — `0` must be an explicit, documented way to turn
/// enforcement off, not an accident of parsing.
/// What: parses as `u64`; `0` maps to `None` (enforcement off); unparseable
/// values fall back to [`DEFAULT_RSS_LIMIT_MB`].
/// Test: `rss_limit_honours_env_override`.