/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 | | } |