Coverage Report

Created: 2026-07-07 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/lyquor/lyquor/platform/api/src/subsys.rs
Line
Count
Source
1
//! Supervised lifecycle and dependency tracking for long-lived node subsystems.
2
//!
3
//! A subsystem is brought up by [`Subsystem::start`], returns a cloneable capability
4
//! [`Subsystem::Handle`] for its dependents, and winds down when its [`SubsysCtx`] is cancelled.
5
//! A [`Supervisor`] records the dependency graph as subsystems are spawned and derives shutdown
6
//! from it: walking the graph in reverse, each subsystem is cancelled and drained as soon as all
7
//! of its dependents have drained, with independent chains stopping in parallel.
8
//!
9
//! Startup is concurrent. [`Supervisor::spawn`] launches each subsystem's `start` in its own
10
//! lifecycle task and returns a [`Running`] promise of the handle, backed by a
11
//! [`tokio::sync::SetOnce`]. A dependent holds `Running` clones of its dependencies and parks on
12
//! [`Running::handle`] inside its own `start`, so it begins the moment the handles it needs exist
13
//! — never earlier, and never serialized behind unrelated subsystems. A failed (or panicked)
14
//! start fails the boot: [`Supervisor::wait_all_ready`] reports the cause, and
15
//! [`Supervisor::shutdown`] aborts the starts still parked on the missing handle.
16
//!
17
//! Dependency edges are declared with [`SubsysId`]s at spawn time. A supervisor is the single
18
//! composition root for one node process, and its ids are plain graph indices scoped to that
19
//! supervisor. Registration is sequential (spawning takes `&mut Supervisor`) even though starting
20
//! is concurrent, so an id only ever names an earlier-registered subsystem and the edges are
21
//! acyclic by construction; only the explicit [`Supervisor::shutdown_order`] overrides need a
22
//! runtime cycle check.
23
//!
24
//! Passive values with no tasks of their own (stores, registries, resolvers) can be recorded with
25
//! [`Supervisor::resource`] so the graph stays complete; they are never "stopped" but ordering
26
//! constraints propagate through them.
27
28
use std::{
29
    future::Future,
30
    sync::Arc,
31
    time::{Duration, Instant},
32
};
33
34
use tokio::sync::SetOnce;
35
use tokio_util::{sync::CancellationToken, task::TaskTracker};
36
37
/// Handle cell shared between a subsystem's lifecycle task and its [`Running`] clones. Set only
38
/// on success; a failed start leaves it forever pending.
39
type HandleCell<H> = Arc<SetOnce<H>>;
40
41
/// Readiness of one subsystem, observed by [`Supervisor::wait_all_ready`]: unset while starting,
42
/// `Ok` once ready, `Err` if the start failed.
43
type ReadyCell = Arc<SetOnce<anyhow::Result<()>>>;
44
45
/// Interval at which the supervisor warns about a subsystem that is slow to start or stop. The
46
/// supervisor keeps waiting: this is observability, not a deadline.
47
const SLOW_WARN_INTERVAL: Duration = Duration::from_secs(10);
48
49
/// A long-lived node component with a supervised lifecycle.
50
///
51
/// The implementing type is the subsystem's *configuration*: it carries the settings and the
52
/// [`Running`] dependencies the subsystem needs and is consumed by [`Subsystem::start`]. "Ready"
53
/// is defined as `start` returning; acquiring dependency handles ([`Running::handle`]), binding
54
/// sockets, replaying state, and other slow readiness all belong inside `start`.
55
///
56
/// Contract for implementations:
57
/// - Acquire dependency handles by holding `Running` clones and awaiting them in `start`.
58
/// - Spawn long-lived work with [`SubsysCtx::spawn`] and make it wind down when the ctx token is
59
///   cancelled. `spawn` intentionally follows [`TaskTracker`] semantics: closing the tracker lets
60
///   the supervisor wait for the current task set, but does not reject later inserts by itself, so
61
///   handles that keep a cloned ctx for dynamic work must gate new work on cancellation.
62
/// - Components with their own lifecycle contract integrate via [`SubsysCtx::on_shutdown`]
63
///   instead.
64
/// - The supervisor owns lifecycle, not health: spawned tasks are cancelled and drained at
65
///   shutdown but never watched for panics or early exits. Handling worker errors — and deciding
66
///   whether one is fatal — is the subsystem's own job; a subsystem whose failure should stop the
67
///   node can hold the root trigger token as ordinary configuration and cancel it.
68
pub trait Subsystem: Send + Sized + 'static {
69
    /// Cloneable capability handle given to dependents. Use `()` if the subsystem exposes no
70
    /// capabilities.
71
    type Handle: Clone + Send + Sync + 'static;
72
73
    /// Stable name used in logs and the dependency graph.
74
    fn name(&self) -> &'static str;
75
76
    /// Bring the subsystem up; return once it is ready to serve.
77
    fn start(self, ctx: SubsysCtx) -> impl Future<Output = anyhow::Result<Self::Handle>> + Send;
78
}
79
80
/// Per-subsystem lifecycle context handed to [`Subsystem::start`].
81
///
82
/// Cheap to clone; subsystems that spawn tasks dynamically at runtime may keep a clone around,
83
/// but should reject new dynamic work once [`Self::shutdown_token`] is cancelled.
84
#[derive(Clone)]
85
pub struct SubsysCtx {
86
    name: &'static str,
87
    shutdown: CancellationToken,
88
    tasks: TaskTracker,
89
}
90
91
lyquor_primitives::debug_struct_name!(SubsysCtx);
92
93
impl SubsysCtx {
94
    /// Token cancelled when this subsystem must stop. Hand out `child_token()`s to inner
95
    /// components that take a `CancellationToken`.
96
25
    pub fn shutdown_token(&self) -> &CancellationToken {
97
25
        &self.shutdown
98
25
    }
99
100
    /// Spawn a task owned by this subsystem. The supervisor cancels the shutdown token and then
101
    /// waits for the tracked task set at this subsystem's turn in the shutdown sequence.
102
    ///
103
    /// Like [`TaskTracker::spawn`], this does not reject inserts after the tracker has been
104
    /// closed. Runtime request paths that keep a cloned [`SubsysCtx`] should check
105
    /// [`CancellationToken::is_cancelled`] before spawning new work.
106
26
    pub fn spawn<F>(&self, task: F) -> tokio::task::JoinHandle<F::Output>
107
26
    where
108
26
        F: Future + Send + 'static,
109
26
        F::Output: Send + 'static,
110
    {
111
26
        self.tasks.spawn(task)
112
26
    }
113
114
    /// Run `teardown` at this subsystem's turn in the shutdown sequence, once its token is
115
    /// cancelled; the subsystem counts as stopped only after it finishes. This is how components
116
    /// with their own lifecycle contract integrate:
117
    /// `ctx.on_shutdown(async move { owner.shutdown().await })`.
118
    ///
119
    /// Multiple teardowns run concurrently — sequence dependent steps inside one future.
120
3
    pub fn on_shutdown<F>(&self, teardown: F)
121
3
    where
122
3
        F: Future<Output = ()> + Send + 'static,
123
    {
124
3
        let stop = self.shutdown.clone();
125
3
        self.tasks.spawn(async move {
126
3
            stop.cancelled().await;
127
3
            teardown.await;
128
3
        });
129
3
    }
130
}
131
132
/// Identifier of a subsystem or resource in a [`Supervisor`]'s dependency graph.
133
///
134
/// Ids are only meaningful for the supervisor that issued them. They are not globally tagged with
135
/// a supervisor identity; mixing ids from different supervisors is unsupported wiring.
136
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
137
pub struct SubsysId(usize);
138
139
/// A spawned subsystem (or registered resource): a promise of its capability handle plus its
140
/// graph id.
141
///
142
/// Cheap to clone regardless of the handle type; subsystem configurations hold `Running` clones
143
/// of their dependencies and await [`Self::handle`] inside `start`.
144
pub struct Running<H> {
145
    id: SubsysId,
146
    name: &'static str,
147
    cell: HandleCell<H>,
148
}
149
150
impl<H> Clone for Running<H> {
151
4
    fn clone(&self) -> Self {
152
4
        Self {
153
4
            id: self.id,
154
4
            name: self.name,
155
4
            cell: self.cell.clone(),
156
4
        }
157
4
    }
158
}
159
160
impl<H> std::fmt::Debug for Running<H> {
161
0
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162
0
        f.debug_struct("Running")
163
0
            .field("name", &self.name)
164
0
            .finish_non_exhaustive()
165
0
    }
166
}
167
168
impl<H: Clone> Running<H> {
169
    /// Graph id of this subsystem, used to declare dependencies on it.
170
18
    pub fn id(&self) -> SubsysId {
171
18
        self.id
172
18
    }
173
174
    /// Wait until the subsystem is ready and clone its capability handle out.
175
    ///
176
    /// If the subsystem fails to start this never resolves: the failure surfaces through
177
    /// [`Supervisor::wait_all_ready`], and [`Supervisor::shutdown`] aborts starts parked here.
178
    /// Await handles inside `start`; in the composition root, only after `wait_all_ready`.
179
5
    pub async fn handle(&self) -> H {
180
5
        self.cell.wait().await.
clone3
()
181
3
    }
182
}
183
184
/// One subsystem or resource registered with the supervisor.
185
struct Entry {
186
    name: &'static str,
187
    /// Stops-before edges (dependencies plus [`Supervisor::shutdown_order`] overrides): this
188
    /// entry stops before each of them.
189
    deps: Vec<usize>,
190
    /// Cancelled at this entry's turn in the shutdown sequence.
191
    stop: CancellationToken,
192
    /// The entry's lifecycle task plus everything spawned via its [`SubsysCtx`]; drained by the
193
    /// supervisor at this entry's turn in the shutdown sequence.
194
    tasks: TaskTracker,
195
    ready: ReadyCell,
196
    /// Passive value with no tasks: never stopped, present only for ordering.
197
    resource: bool,
198
}
199
200
/// Owner of the subsystem dependency graph; derives the shutdown sequence from it.
201
///
202
/// The root [`CancellationToken`] passed to [`Supervisor::new`] is only the *trigger*: cancelling
203
/// it aborts starts that are still pending and is the signal to run [`Supervisor::shutdown`].
204
/// Each subsystem has its own token, cancelled at its turn in the shutdown sequence.
205
///
206
/// Registration takes `&mut self` and is sequential; the spawned subsystems themselves start
207
/// concurrently, gated only by the [`Running::handle`]s they await.
208
pub struct Supervisor {
209
    root: CancellationToken,
210
    entries: Vec<Entry>,
211
}
212
213
lyquor_primitives::debug_struct_name!(Supervisor);
214
215
impl Supervisor {
216
    /// Create a supervisor triggered by `root`.
217
16
    pub fn new(root: CancellationToken) -> Self {
218
16
        Self {
219
16
            root,
220
16
            entries: Vec::new(),
221
16
        }
222
16
    }
223
224
    /// Clone of the root trigger token, for wiring external triggers such as signal handlers.
225
2
    pub fn shutdown_token(&self) -> CancellationToken {
226
2
        self.root.clone()
227
2
    }
228
229
    /// Completes once shutdown has been requested.
230
0
    pub async fn wait_shutdown_triggered(&self) {
231
0
        self.root.cancelled().await;
232
0
    }
233
234
    /// Register a subsystem with dependency edges to `deps` and immediately launch its `start` in
235
    /// a lifecycle task. Returns a [`Running`] promise of its handle.
236
    ///
237
    /// If shutdown is triggered while the start is pending, the start is abandoned and whatever
238
    /// work it already put on its ctx is cleaned up at its turn during [`Self::shutdown`].
239
    ///
240
    /// Must be called from within a tokio runtime.
241
    ///
242
    /// # Panics
243
    /// Panics if a dependency id is out of range for this supervisor. Ids from another supervisor
244
    /// are unsupported and may alias an in-range entry because ids are plain graph indices.
245
32
    pub fn spawn<S: Subsystem>(&mut self, subsys: S, deps: impl IntoIterator<Item = SubsysId>) -> Running<S::Handle> {
246
32
        let name = subsys.name();
247
32
        let deps = self.collect_deps(name, deps);
248
32
        let cell: HandleCell<S::Handle> = Arc::new(SetOnce::new());
249
32
        let ready: ReadyCell = Arc::new(SetOnce::new());
250
32
        let ctx = SubsysCtx {
251
32
            name,
252
32
            shutdown: CancellationToken::new(),
253
32
            tasks: TaskTracker::new(),
254
32
        };
255
32
        let stop = ctx.shutdown.clone();
256
32
        let tasks = ctx.tasks.clone();
257
32
        tasks.spawn(run_subsystem(
258
32
            subsys,
259
32
            ctx,
260
32
            self.root.clone(),
261
32
            cell.clone(),
262
32
            ready.clone(),
263
        ));
264
32
        let id = SubsysId(self.entries.len());
265
32
        self.entries.push(Entry {
266
32
            name,
267
32
            deps,
268
32
            stop,
269
32
            tasks,
270
32
            ready,
271
32
            resource: false,
272
32
        });
273
32
        Running { id, name, cell }
274
32
    }
275
276
    /// Record a passive value (store, registry, resolver, ...) as a graph node.
277
    ///
278
    /// Resources own no tasks and are never stopped, but ordering constraints propagate through
279
    /// them: if task `A` depends on resource `R` and `R` depends on task `B`, then `A` stops
280
    /// before `B`. The returned [`Running::handle`] resolves immediately.
281
    ///
282
    /// # Panics
283
    /// Panics if a dependency id is out of range for this supervisor. Ids from another supervisor
284
    /// are unsupported and may alias an in-range entry because ids are plain graph indices.
285
3
    pub fn resource<T>(&mut self, name: &'static str, value: T, deps: impl IntoIterator<Item = SubsysId>) -> Running<T>
286
3
    where
287
3
        T: Clone + Send + Sync + 'static,
288
    {
289
3
        let deps = self.collect_deps(name, deps);
290
3
        let cell = Arc::new(SetOnce::new());
291
3
        cell.set(value).ok().expect("fresh cell is settable");
292
3
        let ready: ReadyCell = Arc::new(SetOnce::new());
293
3
        ready.set(Ok(())).expect("fresh cell is settable");
294
3
        let id = SubsysId(self.entries.len());
295
3
        self.entries.push(Entry {
296
3
            name,
297
3
            deps,
298
3
            stop: CancellationToken::new(),
299
3
            tasks: TaskTracker::new(),
300
3
            ready,
301
3
            resource: true,
302
3
        });
303
3
        Running { id, name, cell }
304
3
    }
305
306
    /// Add a shutdown-ordering edge with no capability meaning: `first` is cancelled and drained
307
    /// strictly before `later`. Use sparingly, for flow constraints the capability graph cannot
308
    /// express (e.g. "the DB flusher stops only after the state writers stopped").
309
    ///
310
    /// # Panics
311
    /// Panics if the edge contradicts the existing stop order (would form a cycle) or if an id is
312
    /// out of range for this supervisor. Ids from another supervisor are unsupported and may alias
313
    /// an in-range entry because ids are plain graph indices.
314
2
    pub fn shutdown_order(&mut self, first: SubsysId, later: SubsysId) {
315
2
        assert!(
316
2
            first.0 < self.entries.len() && later.0 < self.entries.len(),
317
            "shutdown_order: SubsysId is out of range for this supervisor graph"
318
        );
319
2
        assert_ne!(
320
            first.0, later.0,
321
            "shutdown_order: a subsystem cannot stop before itself"
322
        );
323
2
        assert!(
324
2
            !stops_before(&self.entries, later.0, first.0),
325
            "shutdown_order({} before {}) contradicts the existing stop order",
326
1
            self.entries[first.0].name,
327
1
            self.entries[later.0].name,
328
        );
329
1
        self.entries[first.0].deps.push(later.0);
330
1
    }
331
332
    /// Wait until every spawned subsystem is ready, failing fast on the first one whose start
333
    /// fails. On error the caller should run [`Self::shutdown`] to unwind the subsystems that did
334
    /// start.
335
15
    pub async fn wait_all_ready(&self) -> anyhow::Result<()> {
336
33
        let 
waits15
=
self.entries.iter()15
.
map15
(|entry| async {
337
33
            match entry.ready.wait().await {
338
26
                Ok(()) => Ok(()),
339
5
                Err(error) => Err(anyhow::anyhow!("subsystem {} failed to start: {error:#}", entry.name)),
340
            }
341
64
        });
342
15
        futures::future::try_join_all(waits).await.map(|_| ())
343
15
    }
344
345
    /// Stop all subsystems by walking the graph in reverse: each one is cancelled and drained
346
    /// (its [`SubsysCtx::on_shutdown`] teardowns and spawned tasks) as soon as every subsystem
347
    /// depending on it has drained, so independent chains stop in parallel. Also triggers the
348
    /// root token, so starts that are still pending abort.
349
    ///
350
    /// Dropping the supervisor without calling this (e.g. `?` on a failed
351
    /// [`Self::wait_all_ready`]) cancels every subsystem's token as a safety net, but waits for
352
    /// nothing; `shutdown` is the ordered, drained teardown.
353
    ///
354
    /// This future consumes the supervisor and is intended to be awaited to completion. It is not
355
    /// a cancel-safe operation to race in `tokio::select!` and then drop; enforce external
356
    /// shutdown deadlines at a higher level if the process needs one.
357
14
    pub async fn shutdown(mut self) {
358
14
        self.root.cancel();
359
14
        let started = Instant::now();
360
        // Taken out so the `Drop` safety net sees an already-emptied supervisor.
361
14
        let entries = std::mem::take(&mut self.entries);
362
        // dependents[e] = entries that must stop before entry e
363
14
        let mut dependents = vec![Vec::new(); entries.len()];
364
31
        for (index, entry) in 
entries.iter()14
.
enumerate14
() {
365
31
            for &
dep14
in &entry.deps {
366
14
                dependents[dep].push(index);
367
14
            }
368
        }
369
        // Mirrors the ReadyCell on the way down: set once an entry has fully stopped.
370
31
        let 
stopped14
:
Vec<SetOnce<()>>14
=
entries.iter()14
.
map14
(|_| SetOnce::new()).
collect14
();
371
31
        let 
waits14
=
entries.iter()14
.
enumerate14
().
map14
(|(index, entry)| {
372
31
            let blockers = &dependents[index];
373
31
            let stopped = &stopped;
374
31
            async move {
375
31
                for &
blocker14
in blockers {
376
14
                    stopped[blocker].wait().await;
377
                }
378
31
                if !entry.resource {
379
30
                    entry.stop.cancel();
380
30
                    stop_entry(entry.name, entry.tasks.clone()).await;
381
1
                }
382
31
                let _ = stopped[index].set(());
383
31
            }
384
31
        });
385
14
        futures::future::join_all(waits).await;
386
14
        tracing::info!(
387
0
            elapsed_ms = started.elapsed().as_millis() as u64,
388
            "all subsystems stopped"
389
        );
390
14
    }
391
392
35
    fn collect_deps(&self, name: &'static str, deps: impl IntoIterator<Item = SubsysId>) -> Vec<usize> {
393
35
        let deps: Vec<usize> = deps.into_iter().map(|id| id.0).collect();
394
35
        for &
dep14
in &deps {
395
14
            assert!(
396
14
                dep < self.entries.len(),
397
                "subsystem {name}: dependency SubsysId is out of range for this supervisor graph"
398
            );
399
        }
400
35
        deps
401
35
    }
402
}
403
404
/// Safety net for supervisors dropped without [`Supervisor::shutdown`] (e.g. `?` on a failed
405
/// [`Supervisor::wait_all_ready`]): cancel every subsystem so no work outlives its owner. This
406
/// only signals — nothing is drained or waited for.
407
impl Drop for Supervisor {
408
16
    fn drop(&mut self) {
409
16
        if self.entries.is_empty() {
410
14
            return;
411
2
        }
412
2
        tracing::warn!("supervisor dropped without shutdown; cancelling subsystems without draining them");
413
2
        self.root.cancel();
414
4
        for entry in 
&self.entries2
{
415
4
            entry.stop.cancel();
416
4
        }
417
16
    }
418
}
419
420
/// Runs one subsystem's `start` (abandoning it if shutdown is triggered first) and publishes the
421
/// handle or the failure. Lives on the entry's own tracker like any ctx-spawned task.
422
32
async fn run_subsystem<S: Subsystem>(
423
32
    subsys: S, ctx: SubsysCtx, root: CancellationToken, cell: HandleCell<S::Handle>, ready: ReadyCell,
424
32
) {
425
32
    let name = ctx.name;
426
    // If this task unwinds (subsystem start panicked) before publishing readiness, the guard
427
    // publishes failure so `wait_all_ready` does not hang.
428
32
    let mut guard = PublishOnDrop {
429
32
        ready: &ready,
430
32
        armed: true,
431
32
    };
432
433
32
    let 
publish31
= if root.is_cancelled() {
434
1
        Err(anyhow::anyhow!("shutdown triggered before start"))
435
    } else {
436
31
        tracing::debug!(subsystem = name, "starting subsystem");
437
31
        let started = Instant::now();
438
31
        let start = async {
439
31
            tokio::select! {
440
                biased;
441
31
                
result27
= subsys.start(ctx.clone()) =>
result27
,
442
31
                () = root.cancelled() => 
Err(anyhow::anyhow!("shutdown triggered while subsystem was starting"))3
,
443
            }
444
30
        };
445
31
        let 
result30
= warn_if_slow(name, "start", start).await;
446
30
        if result.is_ok() {
447
25
            tracing::info!(
448
                subsystem = name,
449
0
                elapsed_ms = started.elapsed().as_millis() as u64,
450
                "subsystem ready"
451
            );
452
5
        }
453
30
        result
454
    };
455
31
    guard.armed = false;
456
31
    match publish {
457
25
        Ok(handle) => {
458
25
            let _ = cell.set(handle);
459
25
            let _ = ready.set(Ok(()));
460
25
        }
461
6
        Err(error) => {
462
6
            tracing::error!(
463
                subsystem = name,
464
0
                error = format!("{error:#}"),
465
                "subsystem failed to start"
466
            );
467
6
            let _ = ready.set(Err(error));
468
        }
469
    }
470
31
}
471
472
/// Publishes failure if the lifecycle task unwinds before publishing readiness.
473
struct PublishOnDrop<'a> {
474
    ready: &'a SetOnce<anyhow::Result<()>>,
475
    armed: bool,
476
}
477
478
impl Drop for PublishOnDrop<'_> {
479
32
    fn drop(&mut self) {
480
32
        if self.armed {
481
1
            let _ = self.ready.set(Err(anyhow::anyhow!("panicked while starting")));
482
31
        }
483
32
    }
484
}
485
486
/// Drain an entry's task tracker: its ctx-spawned tasks and [`SubsysCtx::on_shutdown`]
487
/// teardowns (which wake on the just-cancelled stop token).
488
30
async fn stop_entry(name: &'static str, tasks: TaskTracker) {
489
30
    let started = Instant::now();
490
30
    tracing::debug!(subsystem = name, "stopping subsystem");
491
30
    tasks.close();
492
30
    warn_if_slow(name, "stop", tasks.wait()).await;
493
30
    tracing::debug!(
494
        subsystem = name,
495
0
        elapsed_ms = started.elapsed().as_millis() as u64,
496
        "subsystem stopped"
497
    );
498
30
}
499
500
/// Await `operation`, logging a warning every [`SLOW_WARN_INTERVAL`] until it completes.
501
61
async fn warn_if_slow<T>(name: &'static str, phase: &'static str, operation: impl Future<Output = T>) -> T {
502
61
    let started = Instant::now();
503
61
    tokio::pin!(operation);
504
    loop {
505
61
        match tokio::time::timeout(SLOW_WARN_INTERVAL, &mut operation).await {
506
60
            Ok(output) => break output,
507
0
            Err(_) => tracing::warn!(
508
                subsystem = name,
509
                phase,
510
0
                waited_secs = started.elapsed().as_secs(),
511
                "subsystem is taking a long time"
512
            ),
513
        }
514
    }
515
60
}
516
517
/// True if `from` transitively stops before `to` under the current graph.
518
2
fn stops_before(entries: &[Entry], from: usize, to: usize) -> bool {
519
2
    let mut seen = vec![false; entries.len()];
520
2
    let mut stack = vec![from];
521
4
    while let Some(
current3
) = stack.pop() {
522
3
        if current == to {
523
1
            return true;
524
2
        }
525
2
        if std::mem::replace(&mut seen[current], true) {
526
0
            continue;
527
2
        }
528
2
        stack.extend(entries[current].deps.iter().copied());
529
    }
530
1
    false
531
2
}
532
533
#[cfg(test)]
534
mod tests {
535
    use std::sync::atomic::{AtomicUsize, Ordering};
536
537
    use lyquor_test::test;
538
    use tokio::sync::{mpsc, oneshot};
539
540
    use super::*;
541
542
    type Events = mpsc::UnboundedSender<String>;
543
544
    /// Reports lifecycle events, optionally waits for a dependency handle and/or a start gate
545
    /// inside `start`, and can hold up its stop until a stop gate fires.
546
    struct Tracked {
547
        name: &'static str,
548
        events: Events,
549
        needs: Option<Running<()>>,
550
        start_gate: Option<oneshot::Receiver<()>>,
551
        stop_gate: Option<oneshot::Receiver<()>>,
552
    }
553
554
    impl Tracked {
555
25
        fn new(name: &'static str, events: &Events) -> Self {
556
25
            Self {
557
25
                name,
558
25
                events: events.clone(),
559
25
                needs: None,
560
25
                start_gate: None,
561
25
                stop_gate: None,
562
25
            }
563
25
        }
564
565
3
        fn needs(mut self, dep: &Running<()>) -> Self {
566
3
            self.needs = Some(dep.clone());
567
3
            self
568
3
        }
569
570
3
        fn start_gated(mut self) -> (Self, oneshot::Sender<()>) {
571
3
            let (release, gate) = oneshot::channel();
572
3
            self.start_gate = Some(gate);
573
3
            (self, release)
574
3
        }
575
576
9
        fn stop_gated(mut self) -> (Self, oneshot::Sender<()>) {
577
9
            let (release, gate) = oneshot::channel();
578
9
            self.stop_gate = Some(gate);
579
9
            (self, release)
580
9
        }
581
    }
582
583
    impl Subsystem for Tracked {
584
        type Handle = ();
585
586
25
        fn name(&self) -> &'static str {
587
25
            self.name
588
25
        }
589
590
24
        async fn start(self, ctx: SubsysCtx) -> anyhow::Result<()> {
591
            let Self {
592
24
                name,
593
24
                events,
594
24
                needs,
595
24
                start_gate,
596
24
                stop_gate,
597
24
            } = self;
598
24
            events.send(format!("{name}:starting")).unwrap();
599
24
            if let Some(
dep3
) = needs {
600
3
                dep.handle().await;
601
21
            }
602
22
            if let Some(
gate3
) = start_gate {
603
3
                gate.await.ok();
604
19
            }
605
22
            events.send(format!("{name}:started")).unwrap();
606
22
            let token = ctx.shutdown_token().clone();
607
22
            ctx.spawn(async move {
608
22
                token.cancelled().await;
609
22
                events.send(format!("{name}:cancelled")).unwrap();
610
22
                if let Some(
gate9
) = stop_gate {
611
9
                    let _ = gate.await;
612
13
                }
613
22
                events.send(format!("{name}:stopped")).unwrap();
614
22
            });
615
22
            Ok(())
616
22
        }
617
    }
618
619
29
    async fn expect_event(events: &mut mpsc::UnboundedReceiver<String>, expected: &str) {
620
29
        assert_eq!(events.recv().await.as_deref(), Some(expected));
621
29
    }
622
623
6
    async fn expect_events_any_order(events: &mut mpsc::UnboundedReceiver<String>, expected: [&str; 2]) {
624
6
        let mut received = [events.recv().await.unwrap(), events.recv().await.unwrap()];
625
6
        received.sort();
626
6
        let mut expected = expected.map(str::to_string);
627
6
        expected.sort();
628
6
        assert_eq!(received, expected);
629
6
    }
630
631
8
    fn drain(events: &mut mpsc::UnboundedReceiver<String>) {
632
45
        while events.try_recv().is_ok() 
{}37
633
8
    }
634
635
    #[test(tokio::test)]
636
    async fn independent_subsystems_start_concurrently() {
637
        let (events_tx, mut events) = mpsc::unbounded_channel();
638
        let mut sup = Supervisor::new(CancellationToken::new());
639
        let (a_subsys, release_a) = Tracked::new("a", &events_tx).start_gated();
640
        let (b_subsys, release_b) = Tracked::new("b", &events_tx).start_gated();
641
        sup.spawn(a_subsys, []);
642
        sup.spawn(b_subsys, []);
643
644
        // Both starts are in flight at once: each reports `starting` while neither has been
645
        // allowed to finish. A supervisor that awaited each start in turn would hang here.
646
        expect_events_any_order(&mut events, ["a:starting", "b:starting"]).await;
647
        release_a.send(()).unwrap();
648
        release_b.send(()).unwrap();
649
        sup.wait_all_ready().await.unwrap();
650
        expect_events_any_order(&mut events, ["a:started", "b:started"]).await;
651
        sup.shutdown().await;
652
    }
653
654
    #[test(tokio::test)]
655
    async fn dependent_start_waits_for_dependency_handle() {
656
        let (events_tx, mut events) = mpsc::unbounded_channel();
657
        let mut sup = Supervisor::new(CancellationToken::new());
658
        let (a_subsys, release_a) = Tracked::new("a", &events_tx).start_gated();
659
        let a = sup.spawn(a_subsys, []);
660
        sup.spawn(Tracked::new("b", &events_tx).needs(&a), [a.id()]);
661
662
        expect_events_any_order(&mut events, ["a:starting", "b:starting"]).await;
663
        // b is parked on a's handle; nothing further happens until a is released.
664
        assert!(events.try_recv().is_err(), "b must not start before a is ready");
665
        release_a.send(()).unwrap();
666
        sup.wait_all_ready().await.unwrap();
667
        expect_event(&mut events, "a:started").await;
668
        expect_event(&mut events, "b:started").await;
669
        sup.shutdown().await;
670
    }
671
672
    struct Producer;
673
674
    impl Subsystem for Producer {
675
        type Handle = Arc<AtomicUsize>;
676
677
1
        fn name(&self) -> &'static str {
678
1
            "producer"
679
1
        }
680
681
1
        async fn start(self, _ctx: SubsysCtx) -> anyhow::Result<Self::Handle> {
682
1
            Ok(Arc::new(AtomicUsize::new(1)))
683
1
        }
684
    }
685
686
    struct Consumer {
687
        producer: Running<Arc<AtomicUsize>>,
688
    }
689
690
    impl Subsystem for Consumer {
691
        type Handle = ();
692
693
1
        fn name(&self) -> &'static str {
694
1
            "consumer"
695
1
        }
696
697
1
        async fn start(self, _ctx: SubsysCtx) -> anyhow::Result<()> {
698
1
            let counter = self.producer.handle().await;
699
1
            counter.fetch_add(1, Ordering::SeqCst);
700
1
            Ok(())
701
1
        }
702
    }
703
704
    #[test(tokio::test)]
705
    async fn handles_flow_between_subsystems() {
706
        let mut sup = Supervisor::new(CancellationToken::new());
707
        let producer = sup.spawn(Producer, []);
708
        sup.spawn(
709
            Consumer {
710
                producer: producer.clone(),
711
            },
712
            [producer.id()],
713
        );
714
715
        sup.wait_all_ready().await.unwrap();
716
        assert_eq!(producer.handle().await.load(Ordering::SeqCst), 2);
717
        sup.shutdown().await;
718
    }
719
720
    #[test(tokio::test)]
721
    async fn shutdown_stops_dependents_before_dependencies() {
722
        let (events_tx, mut events) = mpsc::unbounded_channel();
723
        let mut sup = Supervisor::new(CancellationToken::new());
724
        let a = sup.spawn(Tracked::new("a", &events_tx), []);
725
        let (b_subsys, release_b) = Tracked::new("b", &events_tx).stop_gated();
726
        let b = sup.spawn(b_subsys, [a.id()]);
727
        let (c_subsys, release_c) = Tracked::new("c", &events_tx).stop_gated();
728
        sup.spawn(c_subsys, [b.id()]);
729
        sup.wait_all_ready().await.unwrap();
730
        drain(&mut events);
731
732
        let shutdown = tokio::spawn(sup.shutdown());
733
        expect_event(&mut events, "c:cancelled").await;
734
        assert!(events.try_recv().is_err(), "b must not be cancelled while c drains");
735
        release_c.send(()).unwrap();
736
        expect_event(&mut events, "c:stopped").await;
737
        expect_event(&mut events, "b:cancelled").await;
738
        assert!(events.try_recv().is_err(), "a must not be cancelled while b drains");
739
        release_b.send(()).unwrap();
740
        expect_event(&mut events, "b:stopped").await;
741
        expect_event(&mut events, "a:cancelled").await;
742
        expect_event(&mut events, "a:stopped").await;
743
        shutdown.await.unwrap();
744
    }
745
746
    #[test(tokio::test)]
747
    async fn layer_members_are_cancelled_together() {
748
        let (events_tx, mut events) = mpsc::unbounded_channel();
749
        let mut sup = Supervisor::new(CancellationToken::new());
750
        let (x_subsys, release_x) = Tracked::new("x", &events_tx).stop_gated();
751
        let (y_subsys, release_y) = Tracked::new("y", &events_tx).stop_gated();
752
        sup.spawn(x_subsys, []);
753
        sup.spawn(y_subsys, []);
754
        sup.wait_all_ready().await.unwrap();
755
        drain(&mut events);
756
757
        let shutdown = tokio::spawn(sup.shutdown());
758
        // Both leaves are cancelled even though neither has finished stopping yet.
759
        expect_events_any_order(&mut events, ["x:cancelled", "y:cancelled"]).await;
760
        release_x.send(()).unwrap();
761
        release_y.send(()).unwrap();
762
        shutdown.await.unwrap();
763
    }
764
765
    #[test(tokio::test)]
766
    async fn independent_chains_stop_without_waiting_for_each_other() {
767
        let (events_tx, mut events) = mpsc::unbounded_channel();
768
        let mut sup = Supervisor::new(CancellationToken::new());
769
        let a = sup.spawn(Tracked::new("a", &events_tx), []);
770
        sup.spawn(Tracked::new("b", &events_tx), [a.id()]);
771
        let x = sup.spawn(Tracked::new("x", &events_tx), []);
772
        let (y_subsys, release_y) = Tracked::new("y", &events_tx).stop_gated();
773
        sup.spawn(y_subsys, [x.id()]);
774
        sup.wait_all_ready().await.unwrap();
775
        drain(&mut events);
776
777
        // The a<-b chain drains to completion while y (same "layer" as b) is still held open:
778
        // layered shutdown would park a behind the b/y layer barrier.
779
        let shutdown = tokio::spawn(sup.shutdown());
780
        let mut seen = Vec::new();
781
        while seen.last().map(String::as_str) != Some("a:stopped") {
782
            seen.push(events.recv().await.unwrap());
783
        }
784
        assert!(
785
5
            !seen.iter().any(|event| event.starts_with("x:")),
786
            "x stopped early: {seen:?}"
787
        );
788
        assert!(
789
            !seen.contains(&"y:stopped".to_string()),
790
            "y drained unexpectedly: {seen:?}"
791
        );
792
        release_y.send(()).unwrap();
793
        expect_event(&mut events, "y:stopped").await;
794
        expect_event(&mut events, "x:cancelled").await;
795
        expect_event(&mut events, "x:stopped").await;
796
        shutdown.await.unwrap();
797
    }
798
799
    #[test(tokio::test)]
800
    async fn resources_link_shutdown_ordering_transitively() {
801
        let (events_tx, mut events) = mpsc::unbounded_channel();
802
        let mut sup = Supervisor::new(CancellationToken::new());
803
        let b = sup.spawn(Tracked::new("b", &events_tx), []);
804
        let store = sup.resource("store", (), [b.id()]);
805
        let (a_subsys, release_a) = Tracked::new("a", &events_tx).stop_gated();
806
        sup.spawn(a_subsys, [store.id()]);
807
        sup.wait_all_ready().await.unwrap();
808
        drain(&mut events);
809
810
        let shutdown = tokio::spawn(sup.shutdown());
811
        expect_event(&mut events, "a:cancelled").await;
812
        assert!(events.try_recv().is_err(), "b must wait for a to stop");
813
        release_a.send(()).unwrap();
814
        expect_event(&mut events, "a:stopped").await;
815
        expect_event(&mut events, "b:cancelled").await;
816
        expect_event(&mut events, "b:stopped").await;
817
        shutdown.await.unwrap();
818
    }
819
820
    #[test(tokio::test)]
821
    async fn shutdown_order_stops_first_before_later() {
822
        let (events_tx, mut events) = mpsc::unbounded_channel();
823
        let mut sup = Supervisor::new(CancellationToken::new());
824
        let (x_subsys, release_x) = Tracked::new("x", &events_tx).stop_gated();
825
        let x = sup.spawn(x_subsys, []);
826
        let y = sup.spawn(Tracked::new("y", &events_tx), []);
827
        sup.shutdown_order(x.id(), y.id());
828
        sup.wait_all_ready().await.unwrap();
829
        drain(&mut events);
830
831
        let shutdown = tokio::spawn(sup.shutdown());
832
        expect_event(&mut events, "x:cancelled").await;
833
        assert!(events.try_recv().is_err(), "y must wait for x to stop");
834
        release_x.send(()).unwrap();
835
        expect_event(&mut events, "x:stopped").await;
836
        expect_event(&mut events, "y:cancelled").await;
837
        expect_event(&mut events, "y:stopped").await;
838
        shutdown.await.unwrap();
839
    }
840
841
    #[test]
842
    fn shutdown_order_rejects_contradictions() {
843
        let mut sup = Supervisor::new(CancellationToken::new());
844
        let a = sup.resource("a", (), []);
845
        let b = sup.resource("b", (), [a.id()]); // b stops before a
846
1
        let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
847
1
            sup.shutdown_order(a.id(), b.id()); // a stops before b: contradiction
848
1
        }))
849
        .unwrap_err();
850
        let message = panic.downcast_ref::<String>().cloned().unwrap_or_default();
851
        assert!(message.contains("contradicts"), "unexpected panic message: {message}");
852
    }
853
854
    #[test(tokio::test)]
855
    async fn entry_waits_for_all_of_its_dependents() {
856
        let (events_tx, mut events) = mpsc::unbounded_channel();
857
        let mut sup = Supervisor::new(CancellationToken::new());
858
        let x = sup.spawn(Tracked::new("x", &events_tx), []);
859
        let (y_subsys, release_y) = Tracked::new("y", &events_tx).stop_gated();
860
        sup.spawn(y_subsys, [x.id()]);
861
        let (z_subsys, release_z) = Tracked::new("z", &events_tx).stop_gated();
862
        sup.spawn(z_subsys, [x.id()]);
863
        sup.wait_all_ready().await.unwrap();
864
        drain(&mut events);
865
866
        let shutdown = tokio::spawn(sup.shutdown());
867
        expect_events_any_order(&mut events, ["y:cancelled", "z:cancelled"]).await;
868
        release_y.send(()).unwrap();
869
        expect_event(&mut events, "y:stopped").await;
870
        assert!(events.try_recv().is_err(), "x must wait for z as well");
871
        release_z.send(()).unwrap();
872
        expect_event(&mut events, "z:stopped").await;
873
        expect_event(&mut events, "x:cancelled").await;
874
        expect_event(&mut events, "x:stopped").await;
875
        shutdown.await.unwrap();
876
    }
877
878
    struct FailAfterSpawning {
879
        events: Events,
880
    }
881
882
    impl Subsystem for FailAfterSpawning {
883
        type Handle = ();
884
885
2
        fn name(&self) -> &'static str {
886
2
            "failing"
887
2
        }
888
889
2
        async fn start(self, ctx: SubsysCtx) -> anyhow::Result<()> {
890
2
            let events = self.events.clone();
891
2
            let token = ctx.shutdown_token().clone();
892
2
            ctx.spawn(async move {
893
2
                token.cancelled().await;
894
2
                events.send("failing:task-cancelled".into()).unwrap();
895
2
            });
896
2
            let events = self.events;
897
2
            ctx.on_shutdown(async move {
898
2
                events.send("failing:teardown".into()).unwrap();
899
2
            });
900
2
            anyhow::bail!("boom")
901
2
        }
902
    }
903
904
    #[test(tokio::test)]
905
    async fn failed_start_fails_the_boot_and_unwinds_partial_work() {
906
        let (events_tx, mut events) = mpsc::unbounded_channel();
907
        let mut sup = Supervisor::new(CancellationToken::new());
908
        let a = sup.spawn(Tracked::new("a", &events_tx), []);
909
        let failing = sup.spawn(
910
            FailAfterSpawning {
911
                events: events_tx.clone(),
912
            },
913
            [a.id()],
914
        );
915
        // A dependent of the failed subsystem stays parked on its handle; the boot barrier
916
        // reports the root cause and shutdown aborts the parked start.
917
        sup.spawn(Tracked::new("dependent", &events_tx).needs(&failing), [failing.id()]);
918
919
        let error = sup.wait_all_ready().await.unwrap_err();
920
        assert!(
921
            error.to_string().contains("subsystem failing failed to start: boom"),
922
            "unexpected error: {error}"
923
        );
924
        drain(&mut events);
925
926
        sup.shutdown().await;
927
        // The failed subsystem's partial work is cleaned up in its layer (its teardown and task
928
        // run concurrently), then `a` stops.
929
        expect_events_any_order(&mut events, ["failing:teardown", "failing:task-cancelled"]).await;
930
        expect_event(&mut events, "a:cancelled").await;
931
        expect_event(&mut events, "a:stopped").await;
932
    }
933
934
    #[test(tokio::test)]
935
    async fn dropping_the_supervisor_cancels_subsystems() {
936
        let (events_tx, mut events) = mpsc::unbounded_channel();
937
        let mut sup = Supervisor::new(CancellationToken::new());
938
        sup.spawn(Tracked::new("a", &events_tx), []);
939
        sup.spawn(
940
            FailAfterSpawning {
941
                events: events_tx.clone(),
942
            },
943
            [],
944
        );
945
        sup.wait_all_ready().await.unwrap_err();
946
        drain(&mut events);
947
948
        // The natural `?` path: the supervisor is dropped without shutdown(). Every subsystem's
949
        // partial work must still be cancelled rather than outliving its owner.
950
        drop(sup);
951
        let mut seen: Vec<String> = Vec::new();
952
        for _ in 0..4 {
953
            seen.push(events.recv().await.unwrap());
954
        }
955
        seen.sort();
956
        assert_eq!(
957
            seen,
958
            ["a:cancelled", "a:stopped", "failing:task-cancelled", "failing:teardown"]
959
        );
960
    }
961
962
    struct PanicsWhileStarting;
963
964
    impl Subsystem for PanicsWhileStarting {
965
        type Handle = ();
966
967
1
        fn name(&self) -> &'static str {
968
1
            "panicking"
969
1
        }
970
971
1
        async fn start(self, _ctx: SubsysCtx) -> anyhow::Result<()> {
972
1
            panic!("boom");
973
        }
974
    }
975
976
    #[test(tokio::test)]
977
    async fn panicking_start_fails_the_boot_instead_of_hanging() {
978
        let (events_tx, _events) = mpsc::unbounded_channel();
979
        let mut sup = Supervisor::new(CancellationToken::new());
980
        let panicking = sup.spawn(PanicsWhileStarting, []);
981
        sup.spawn(
982
            Tracked::new("dependent", &events_tx).needs(&panicking),
983
            [panicking.id()],
984
        );
985
986
        let error = sup.wait_all_ready().await.unwrap_err();
987
        assert!(
988
            error.to_string().contains("panicking failed to start: panicked"),
989
            "unexpected error: {error}"
990
        );
991
        sup.shutdown().await;
992
    }
993
994
    struct HangsWhileStarting {
995
        events: Events,
996
    }
997
998
    impl Subsystem for HangsWhileStarting {
999
        type Handle = ();
1000
1001
1
        fn name(&self) -> &'static str {
1002
1
            "hanging"
1003
1
        }
1004
1005
1
        async fn start(self, ctx: SubsysCtx) -> anyhow::Result<()> {
1006
1
            let Self { events } = self;
1007
1
            events.send("hanging:entered".into()).unwrap();
1008
1
            let token = ctx.shutdown_token().clone();
1009
1
            ctx.spawn(async move {
1010
1
                token.cancelled().await;
1011
1
                events.send("hanging:task-cancelled".into()).unwrap();
1012
1
            });
1013
1
            std::future::pending::<()>().await;
1014
0
            Ok(())
1015
0
        }
1016
    }
1017
1018
    #[test(tokio::test)]
1019
    async fn triggered_shutdown_aborts_pending_start() {
1020
        let (events_tx, mut events) = mpsc::unbounded_channel();
1021
        let mut sup = Supervisor::new(CancellationToken::new());
1022
        sup.spawn(
1023
            HangsWhileStarting {
1024
                events: events_tx.clone(),
1025
            },
1026
            [],
1027
        );
1028
        expect_event(&mut events, "hanging:entered").await;
1029
1030
        sup.shutdown_token().cancel();
1031
        let error = sup.wait_all_ready().await.unwrap_err();
1032
        assert!(
1033
            error.to_string().contains("shutdown triggered"),
1034
            "unexpected error: {error}"
1035
        );
1036
1037
        // The abandoned subsystem was still registered; shutdown cleans up its spawned work.
1038
        sup.shutdown().await;
1039
        expect_event(&mut events, "hanging:task-cancelled").await;
1040
    }
1041
1042
    struct OwnerTeardown {
1043
        events: Events,
1044
    }
1045
1046
    impl Subsystem for OwnerTeardown {
1047
        type Handle = ();
1048
1049
1
        fn name(&self) -> &'static str {
1050
1
            "owner"
1051
1
        }
1052
1053
1
        async fn start(self, ctx: SubsysCtx) -> anyhow::Result<()> {
1054
1
            let (torn_down_tx, torn_down) = oneshot::channel();
1055
1
            let events = self.events.clone();
1056
1
            ctx.on_shutdown(async move {
1057
1
                events.send("owner:teardown".into()).unwrap();
1058
1
                let _ = torn_down_tx.send(());
1059
1
            });
1060
1
            let events = self.events;
1061
1
            ctx.spawn(async move {
1062
                // Exits only after the teardown ran: shutdown would hang here otherwise, proving
1063
                // the layer waits for on_shutdown teardowns like any other tracked task.
1064
1
                let _ = torn_down.await;
1065
1
                events.send("owner:task-exited".into()).unwrap();
1066
1
            });
1067
1
            Ok(())
1068
1
        }
1069
    }
1070
1071
    #[test(tokio::test)]
1072
    async fn on_shutdown_runs_at_the_stop_turn() {
1073
        let (events_tx, mut events) = mpsc::unbounded_channel();
1074
        let mut sup = Supervisor::new(CancellationToken::new());
1075
        sup.spawn(
1076
            OwnerTeardown {
1077
                events: events_tx.clone(),
1078
            },
1079
            [],
1080
        );
1081
        sup.wait_all_ready().await.unwrap();
1082
        assert!(events.try_recv().is_err(), "teardown must not run before shutdown");
1083
1084
        sup.shutdown().await;
1085
        expect_event(&mut events, "owner:teardown").await;
1086
        expect_event(&mut events, "owner:task-exited").await;
1087
    }
1088
1089
    #[test(tokio::test)]
1090
    async fn spawn_after_trigger_fails_fast() {
1091
        let (events_tx, _events) = mpsc::unbounded_channel();
1092
        let mut sup = Supervisor::new(CancellationToken::new());
1093
        sup.shutdown_token().cancel();
1094
        sup.spawn(Tracked::new("late", &events_tx), []);
1095
        let error = sup.wait_all_ready().await.unwrap_err();
1096
        assert!(
1097
            error.to_string().contains("subsystem late failed to start"),
1098
            "unexpected error: {error}"
1099
        );
1100
        sup.shutdown().await;
1101
    }
1102
}