saluki_core/runtime/builder.rs
1//! Builder for describing children.
2//!
3//! [`ChildSpecification`] is the complete description of a child, and this builder is the only way to configure one:
4//! the specification itself carries no public methods. The builder turns an asynchronous function, or a
5//! [`Supervisable`], into a child with sensible defaults, and uses the typestate pattern to expose only the settings
6//! that make sense for the kind of worker being described.
7//!
8//! It comes in matching ambient and explicit forms, mirroring the two ways to spawn: the free functions here
9//! ([`worker`], [`supervisable`]) target the ambient supervisor, and the identically named methods on
10//! [`SupervisorHandle`] target that handle's supervisor. Either way, [`ChildBuilder::spawn`] starts the child on a
11//! running supervisor, while [`ChildBuilder::build`] hands it to [`Supervisor::add_worker`][add_worker] to be started
12//! when that supervisor runs.
13//!
14//! [add_worker]: super::Supervisor::add_worker
15//!
16//! # Child lifecycle, and one-shot vs supervisable
17//!
18//! We classify tasks as either _one-shot_ or _supervisable_: one-shot tasks are those based on a provided closure,
19//! which cannot be reinitialized and so cannot be restarted, and supervisable tasks are those based on an
20//! implementation of [`Supervisable`], which allows for (potentially) initializing the underlying task future multiple
21//! times.
22//!
23//! One-shot tasks are always [`temporary`][RestartType::Temporary], since they cannot be reinitialized. Supervisable
24//! tasks are [`permanent`][RestartType::Permanent] by default, since their structure exposes a way to initialize the
25//! worker again; [`ChildBuilder::transient`] and [`ChildBuilder::temporary`] narrow that.
26//!
27//! All child tasks default to being marked as non-significant, so their termination -- clean or otherwise -- leaves
28//! the supervisor running. This is usually the correct behavior, but a supervisor that cannot function without a
29//! particular child may wish to mark it significant. See [`ChildBuilder::with_significant`], which is available only
30//! for a child whose restart policy lets it terminate for good; a permanent child is always brought back, so there is
31//! no termination for the supervisor to act on.
32//!
33//! # Shutdown
34//!
35//! Shutting a subtree down is a _trigger_, not an enforcement. A one-shot child is never handed the shutdown signal at
36//! all: it runs until it reaches its own terminal condition -- an input channel closing, a loop finishing, a request
37//! completing -- and the supervisor waits for it. That is what lets a set of tasks connected by channels drain in
38//! dependency order without any of them having to know that order, and it is why the shutdown signal being invisible
39//! here costs nothing.
40//!
41//! What bounds the wait is the supervisor's [shutdown budget][super::Supervisor::with_shutdown_budget], which covers
42//! the whole set of children rather than each guessing at how long it ought to take. A supervisor without a budget has
43//! nothing to bound such a child with, so it falls back to the worker's own strategy.
44//!
45//! Two cases need something other than the default:
46//!
47//! - Work with no terminal condition -- an endless background loop -- would hold the drain until the budget elapsed.
48//! Give it [`ShutdownStrategy::Brutal`] via [`ChildBuilder::with_shutdown_strategy`] so it is aborted at once.
49//! - Work that must observe shutdown to know it should stop, or that needs to run cleanup, isn't a one-shot worker at
50//! all: implement [`Supervisable`] directly, which does receive the signal.
51//!
52//! [`ChildBuilder::with_shutdown_timeout`] imposes a deadline shorter than the budget when a particular child should
53//! be abandoned sooner than its siblings.
54//!
55//! # Task naming
56//!
57//! Child task names should generally _not_ contain unique patterns/tokens -- such as monotonic IDs or high-cardinality
58//! values -- as they are used for internal telemetry about the task. Generally, task names should be thought of as a
59//! category label: if a supervisor manages tasks for handling connections, it should prefer to name them like
60//! `conn_handler` instead of `conn_handler_<ID or IP>`.
61
62use std::{future::Future, marker::PhantomData, time::Duration};
63
64use tokio::runtime::Handle;
65
66use super::{
67 ChildId, ChildSpecification, FnWorker, IntoWorkerResult, RestartType, ShutdownStrategy, Supervisable, Supervisor,
68 SupervisorHandle, SupervisorSpec, WorkerSpec,
69};
70
71/// Creates a builder for a child task on the ambient supervisor.
72///
73/// The ambient counterpart to [`SupervisorHandle::worker`].
74///
75/// # Panics
76///
77/// [`ChildBuilder::spawn`] panics if there is no ambient supervisor. See [`spawn`][super::spawn].
78pub fn worker<N, Fut>(name: N, fut: Fut) -> ChildBuilder<'static>
79where
80 N: Into<String>,
81 Fut: Future + Send + 'static,
82 Fut::Output: IntoWorkerResult,
83{
84 ChildBuilder::one_shot(BuilderTarget::Ambient, FnWorker::new(name, fut))
85}
86
87/// Creates a builder for a supervisable child task on the ambient supervisor.
88///
89/// The ambient counterpart to [`SupervisorHandle::supervisable`], which documents what a supervisable task is.
90///
91/// # Panics
92///
93/// [`ChildBuilder::spawn`] panics if there is no ambient supervisor. See [`spawn`][super::spawn].
94pub fn supervisable<T>(worker: T) -> ChildBuilder<'static, Restartable>
95where
96 T: Supervisable + 'static,
97{
98 ChildBuilder::restartable(BuilderTarget::Ambient, worker)
99}
100
101impl SupervisorHandle {
102 /// Creates a builder for a child task built from a plain future.
103 ///
104 /// The task runs until it reaches its own terminal condition; it is never handed the shutdown signal. See
105 /// [`FnWorker`] for what that means at shutdown, and for the two cases that need something else.
106 ///
107 /// Use this method when advanced configuration of the underlying task is required. Otherwise, prefer
108 /// [`spawn_worker`][Self::spawn_worker].
109 ///
110 /// # Examples
111 ///
112 /// ```no_run
113 /// # use saluki_core::runtime::SupervisorHandle;
114 /// # async fn encode() {}
115 /// # fn example(supervisor: &SupervisorHandle, pool: tokio::runtime::Handle) {
116 /// supervisor.worker("encoder", encode()).on_runtime(pool).spawn();
117 /// # }
118 /// ```
119 pub fn worker<N, Fut>(&self, name: N, fut: Fut) -> ChildBuilder<'_>
120 where
121 N: Into<String>,
122 Fut: Future + Send + 'static,
123 Fut::Output: IntoWorkerResult,
124 {
125 ChildBuilder::one_shot(BuilderTarget::Handle(self), FnWorker::new(name, fut))
126 }
127
128 /// Creates a builder for a supervisable child task.
129 ///
130 /// Supervisable tasks are those where the worker already implements [`Supervisable`], which lets the builder serve
131 /// as a consistent control surface for spawning both arbitrary asynchronous functions and more full-fledged
132 /// workers.
133 ///
134 /// Supervisable tasks are set to permanently restart by default.
135 ///
136 /// Use this method when advanced configuration of the underlying task is required. Otherwise, prefer
137 /// [`spawn_supervisable`][Self::spawn_supervisable].
138 pub fn supervisable<T>(&self, worker: T) -> ChildBuilder<'_, Restartable>
139 where
140 T: Supervisable + 'static,
141 {
142 ChildBuilder::restartable(BuilderTarget::Handle(self), worker)
143 }
144
145 /// Spawns a child task built from a plain future.
146 ///
147 /// The task runs until it reaches its own terminal condition; it is never handed the shutdown signal. See
148 /// [`FnWorker`] for what that means at shutdown, and for the two cases that need something else.
149 ///
150 /// Use [`worker`][Self::worker] when advanced configuration of the underlying task is required.
151 pub fn spawn_worker<N, Fut>(&self, name: N, fut: Fut) -> ChildId
152 where
153 N: Into<String>,
154 Fut: Future + Send + 'static,
155 Fut::Output: IntoWorkerResult,
156 {
157 self.worker(name, fut).spawn()
158 }
159
160 /// Spawns a supervisable child task.
161 ///
162 /// Supervisable tasks are those where the worker already implements [`Supervisable`], which lets the builder serve
163 /// as a consistent control surface for spawning both arbitrary asynchronous functions and more full-fledged
164 /// workers.
165 ///
166 /// Use [`supervisable`][Self::supervisable] when advanced configuration of the underlying task is required.
167 pub fn spawn_supervisable<T>(&self, worker: T) -> ChildId
168 where
169 T: Supervisable + 'static,
170 {
171 self.supervisable(worker).spawn()
172 }
173
174 /// Creates a builder for a nested supervisor.
175 ///
176 /// A [`Supervisor`] can be handed to [`spawn`][Self::spawn] or [`Supervisor::add_worker`] directly, which is all
177 /// most callers need. This builder exists for the settings that aren't reachable that way: the restart policy and
178 /// significance. It matters most for a dynamically spawned subtree, which would otherwise be
179 /// [`temporary`][RestartType::Temporary] and so quietly stay dead once it terminated.
180 ///
181 /// Unlike [`supervisable`][Self::supervisable], there is no placement or shutdown setting. A nested supervisor
182 /// runs wherever its parent does and its children carry their own placement, and it bounds its own drain through
183 /// those children rather than through a deadline imposed from above.
184 ///
185 /// Nested supervisors are set to permanently restart by default.
186 pub fn nested_supervisor(&self, supervisor: Supervisor) -> NestedSupervisorBuilder<'_> {
187 NestedSupervisorBuilder::new(BuilderTarget::Handle(self), supervisor)
188 }
189}
190
191/// Creates a builder for a nested supervisor on the ambient supervisor.
192///
193/// The ambient counterpart to [`SupervisorHandle::nested_supervisor`], which documents what the builder is for.
194///
195/// # Panics
196///
197/// [`NestedSupervisorBuilder::spawn`] panics if there is no ambient supervisor. See [`spawn`][super::spawn].
198pub fn nested_supervisor(supervisor: Supervisor) -> NestedSupervisorBuilder<'static> {
199 NestedSupervisorBuilder::new(BuilderTarget::Ambient, supervisor)
200}
201
202mod sealed {
203 pub trait Sealed {}
204}
205
206/// The kind of worker a [`ChildBuilder`] is describing.
207///
208/// This trait is sealed, and exists only to mark which configuration a builder makes available: a worker that can be
209/// initialized more than once accepts a restart policy, and one that can't doesn't. Implemented by [`OneShot`],
210/// [`Restartable`] and [`Terminable`].
211pub trait BuilderState: sealed::Sealed {}
212
213/// Marks a builder whose child can terminate without being restarted.
214///
215/// This trait is sealed, and exists only to gate [`ChildBuilder::with_significant`]: significance asks what the
216/// supervisor should do when a child terminates and isn't brought back, so it means nothing for a child that always
217/// is. Implemented by [`OneShot`] and [`Terminable`].
218pub trait CanTerminate: BuilderState {}
219
220/// Marks a builder whose worker can only be initialized once.
221///
222/// Closure-based children ([`SupervisorHandle::worker`]) consume their body when they start, so they can never be
223/// restarted: they are always [`RestartType::Temporary`], and no restart policy is offered.
224pub struct OneShot;
225
226/// Marks a builder whose worker can be initialized more than once.
227///
228/// A [`Supervisable`] builds its work in [`initialize`][Supervisable::initialize] each time it starts, so it can be
229/// restarted, and it is [`RestartType::Permanent`] unless narrowed with [`ChildBuilder::transient`] or
230/// [`ChildBuilder::temporary`].
231pub struct Restartable;
232
233/// Marks a builder whose worker can be initialized more than once but has been narrowed to a restart policy that lets
234/// it terminate for good.
235///
236/// Reached from [`Restartable`] via [`ChildBuilder::transient`] or [`ChildBuilder::temporary`]. Narrowing is one-way:
237/// there is no route back to [`Restartable`], which is what keeps a child from being marked significant and then
238/// widened to [`RestartType::Permanent`] behind the flag's back.
239pub struct Terminable;
240
241impl sealed::Sealed for OneShot {}
242impl BuilderState for OneShot {}
243impl CanTerminate for OneShot {}
244impl sealed::Sealed for Restartable {}
245impl BuilderState for Restartable {}
246impl sealed::Sealed for Terminable {}
247impl BuilderState for Terminable {}
248impl CanTerminate for Terminable {}
249
250/// Builder for a yet-to-be-started child task.
251///
252/// This is the only way to configure a child: [`ChildSpecification`], which this produces, carries no settings of its
253/// own. The builder uses the typestate pattern to expose only the properties that make sense for the child being
254/// described, so an invalid combination doesn't need rejecting at runtime because it can't be written down. Two axes
255/// govern that: whether the worker can be initialized more than once (which decides whether a restart policy is
256/// offered), and whether its policy lets it terminate for good (which decides whether it can be marked significant).
257///
258/// See [`BuilderState`] and [`CanTerminate`] for the states themselves.
259#[must_use = "a child is only described until `spawn` or `build` is called"]
260pub struct ChildBuilder<'a, S = OneShot> {
261 target: BuilderTarget<'a>,
262 spec: ChildSpecification<WorkerSpec>,
263 _state: PhantomData<S>,
264}
265
266/// Which supervisor a [`ChildBuilder`] spawns onto.
267#[derive(Clone, Copy)]
268enum BuilderTarget<'a> {
269 /// Whichever supervisor is ambient when the child is spawned.
270 Ambient,
271
272 /// This specific supervisor.
273 Handle(&'a SupervisorHandle),
274}
275
276impl<'a, S: BuilderState> ChildBuilder<'a, S> {
277 fn new(target: BuilderTarget<'a>, spec: ChildSpecification<WorkerSpec>) -> Self {
278 Self {
279 target,
280 spec,
281 _state: PhantomData,
282 }
283 }
284
285 fn map_spec<F>(self, f: F) -> Self
286 where
287 F: FnOnce(ChildSpecification<WorkerSpec>) -> ChildSpecification<WorkerSpec>,
288 {
289 self.map_spec_into(f)
290 }
291
292 /// As [`map_spec`][Self::map_spec], but for a transition that also moves the builder to a different state.
293 fn map_spec_into<F, S2>(self, f: F) -> ChildBuilder<'a, S2>
294 where
295 F: FnOnce(ChildSpecification<WorkerSpec>) -> ChildSpecification<WorkerSpec>,
296 S2: BuilderState,
297 {
298 let Self { target, spec, .. } = self;
299
300 ChildBuilder::new(target, f(spec))
301 }
302
303 /// Runs this child task on a specific runtime.
304 ///
305 /// Use this for compute-heavy work -- encoding, serialization, protocol servers -- that shouldn't contend with the
306 /// runtime driving the supervisor and its I/O. Only where the task runs changes: the child is still supervised
307 /// here, and is still shut down and restarted by this supervisor.
308 pub fn on_runtime(self, handle: Handle) -> Self {
309 self.map_spec(|spec| spec.with_runtime(handle))
310 }
311
312 /// Sets an explicit shutdown timeout for this child task.
313 ///
314 /// By default a closure-based child has no deadline of its own and is bounded only by the supervisor's shutdown
315 /// budget. Set this when the supervisor wants a particular child abandoned sooner than that -- a deadline it is
316 /// deliberately imposing, rather than a guess at how long the child ought to take. A value longer than the budget
317 /// has no effect, since the two are resolved to whichever elapses first.
318 ///
319 /// For a [`Supervisable`] child, this overrides the strategy the task reports for itself.
320 pub fn with_shutdown_timeout(self, timeout: Duration) -> Self {
321 self.with_shutdown_strategy(ShutdownStrategy::Graceful(timeout))
322 }
323
324 /// Sets the explicit shutdown strategy used for this child task.
325 pub fn with_shutdown_strategy(self, strategy: ShutdownStrategy) -> Self {
326 self.map_spec(|spec| spec.with_shutdown_strategy(strategy))
327 }
328
329 /// Gives this child no shutdown deadline of its own, leaving it bounded solely by the supervisor's shutdown
330 /// budget.
331 ///
332 /// This is already the default for a closure-based child. A [`Supervisable`] child reports its own strategy, so
333 /// this is how one opts into being bounded as part of the group instead -- without which it silently keeps
334 /// whatever [`Supervisable::shutdown_strategy`] returns, which may be far shorter than the budget.
335 ///
336 /// See [`Supervisor::with_shutdown_budget`][budget] for what the budget itself is.
337 ///
338 /// [budget]: super::Supervisor::with_shutdown_budget
339 pub fn with_budget_bounded_shutdown(self) -> Self {
340 self.map_spec(|spec| spec.with_budget_bounded_shutdown())
341 }
342
343 /// Finishes describing the child without starting it, for [`Supervisor::add_worker`].
344 ///
345 /// Use this to register a configured child on a supervisor that hasn't started yet; a child described this way is
346 /// started when the supervisor runs, and restarted with it. [`spawn`][Self::spawn] is the counterpart for a
347 /// supervisor that is already running.
348 ///
349 /// Whichever supervisor this builder was created against is irrelevant here -- the child belongs to whichever one
350 /// it is handed to.
351 ///
352 /// [`Supervisor::add_worker`]: super::Supervisor::add_worker
353 pub fn build(self) -> ChildSpecification<WorkerSpec> {
354 self.spec
355 }
356
357 /// Spawns the child.
358 ///
359 /// Returns the child's [`ChildId`]. The child is queued for the supervisor rather than started synchronously, so
360 /// it may not be running yet by the time this returns; if the supervisor isn't running, or shuts down before
361 /// reaching the child, it never runs at all.
362 ///
363 /// # Panics
364 ///
365 /// If this builder targets the ambient supervisor and there isn't one, this panics. See [`spawn`][super::spawn].
366 pub fn spawn(self) -> ChildId {
367 let Self { target, spec, .. } = self;
368
369 match target {
370 BuilderTarget::Ambient => super::spawn(spec),
371 BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
372 }
373 }
374}
375
376impl<'a, S: CanTerminate> ChildBuilder<'a, S> {
377 /// Sets whether this child task's termination should stop the supervisor.
378 ///
379 /// Under [`AutoShutdown::AnySignificant`][auto_shutdown] -- which is what a topology component's supervisor uses
380 /// -- a significant child terminating **shuts the supervisor down**: the child is not individually restarted. That
381 /// happens even when the child exits cleanly, so this suits a child the supervisor cannot function without, and
382 /// not one that is expected to finish on its own.
383 ///
384 /// For example, a component handling client connections generally shouldn't stop just because one connection
385 /// failed, but a component forwarding work to a child task may become inoperable if that task dies and cannot be
386 /// reattached to the necessary channels or state without recreating the component.
387 ///
388 /// Offered only for a child that can actually terminate for good -- a one-shot child, or a supervisable one
389 /// narrowed with [`transient`][ChildBuilder::transient] or [`temporary`][ChildBuilder::temporary]. A permanent
390 /// child is always restarted and so never reaches this path, which makes marking one significant a contradiction
391 /// rather than a setting.
392 ///
393 /// Defaults to `false`.
394 ///
395 /// [auto_shutdown]: super::AutoShutdown::AnySignificant
396 pub fn with_significant(self, significant: bool) -> Self {
397 self.map_spec(|spec| spec.with_significant(significant))
398 }
399}
400
401impl<'a> ChildBuilder<'a, OneShot> {
402 fn one_shot<T>(target: BuilderTarget<'a>, worker: T) -> Self
403 where
404 T: Supervisable + 'static,
405 {
406 let spec = ChildSpecification::one_shot_worker(worker).with_budget_bounded_shutdown();
407
408 Self::new(target, spec)
409 }
410}
411
412impl<'a> ChildBuilder<'a, Restartable> {
413 fn restartable<T>(target: BuilderTarget<'a>, worker: T) -> Self
414 where
415 T: Supervisable + 'static,
416 {
417 Self::new(
418 target,
419 ChildSpecification::worker(worker).with_restart_type(RestartType::Permanent),
420 )
421 }
422
423 /// Restarts this child task only when it terminates abnormally.
424 ///
425 /// A clean exit is taken at face value and the child stays stopped; a failure is restarted. Use this for work that
426 /// has a natural end but whose failure means it never got there.
427 ///
428 /// Narrows the restart policy to [`RestartType::Transient`], which makes
429 /// [`with_significant`][ChildBuilder::with_significant] available: a child that can stop for good is one whose
430 /// termination the supervisor may want to act on.
431 pub fn transient(self) -> ChildBuilder<'a, Terminable> {
432 self.map_spec_into(|spec| spec.with_restart_type(RestartType::Transient))
433 }
434
435 /// Never restarts this child task.
436 ///
437 /// However it terminates -- cleanly or by failing -- the child stays stopped. Use this for work that is meant to
438 /// run once, where a retry would be wrong rather than merely redundant.
439 ///
440 /// Narrows the restart policy to [`RestartType::Temporary`], which makes
441 /// [`with_significant`][ChildBuilder::with_significant] available: a child that can stop for good is one whose
442 /// termination the supervisor may want to act on.
443 pub fn temporary(self) -> ChildBuilder<'a, Terminable> {
444 self.map_spec_into(|spec| spec.with_restart_type(RestartType::Temporary))
445 }
446}
447
448/// Builder for a yet-to-be-started nested supervisor.
449///
450/// The counterpart to [`ChildBuilder`] for a child that is itself a [`Supervisor`], and deliberately a much smaller
451/// surface. A nested supervisor has no placement of its own (it runs wherever its parent does, and its children carry
452/// their own placement) and no shutdown deadline of its own (it bounds its own drain through its children), so what
453/// is left to configure is the restart policy and significance.
454///
455/// Uses the same typestate as [`ChildBuilder`]: significance is offered only once the restart policy has been
456/// narrowed to one that lets the child terminate for good, since a permanent child is always brought back and so
457/// never has a termination for the parent to act on.
458#[must_use = "a child is only described until `spawn` or `build` is called"]
459pub struct NestedSupervisorBuilder<'a, S = Restartable> {
460 target: BuilderTarget<'a>,
461 spec: ChildSpecification<SupervisorSpec>,
462 _state: PhantomData<S>,
463}
464
465impl<'a, S: BuilderState> NestedSupervisorBuilder<'a, S> {
466 fn from_parts(target: BuilderTarget<'a>, spec: ChildSpecification<SupervisorSpec>) -> Self {
467 Self {
468 target,
469 spec,
470 _state: PhantomData,
471 }
472 }
473
474 /// As [`map_spec`][ChildBuilder::map_spec], but for a transition that also moves the builder to a different state.
475 fn map_spec_into<F, S2>(self, f: F) -> NestedSupervisorBuilder<'a, S2>
476 where
477 F: FnOnce(ChildSpecification<SupervisorSpec>) -> ChildSpecification<SupervisorSpec>,
478 S2: BuilderState,
479 {
480 let Self { target, spec, .. } = self;
481
482 NestedSupervisorBuilder::from_parts(target, f(spec))
483 }
484
485 /// Finishes describing the child without starting it, for [`Supervisor::add_worker`].
486 ///
487 /// Use this to register a configured subtree on a supervisor that hasn't started yet; [`spawn`][Self::spawn] is
488 /// the counterpart for a supervisor that is already running.
489 ///
490 /// Whichever supervisor this builder was created against is irrelevant here -- the child belongs to whichever one
491 /// it is handed to.
492 pub fn build(self) -> ChildSpecification<SupervisorSpec> {
493 self.spec
494 }
495
496 /// Spawns the nested supervisor.
497 ///
498 /// Returns the child's [`ChildId`]. As with [`ChildBuilder::spawn`], the child is queued rather than started
499 /// synchronously.
500 ///
501 /// # Panics
502 ///
503 /// If this builder targets the ambient supervisor and there isn't one, this panics. See [`spawn`][super::spawn].
504 pub fn spawn(self) -> ChildId {
505 let Self { target, spec, .. } = self;
506
507 match target {
508 BuilderTarget::Ambient => super::spawn(spec),
509 BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
510 }
511 }
512}
513
514impl<'a> NestedSupervisorBuilder<'a, Restartable> {
515 fn new(target: BuilderTarget<'a>, supervisor: Supervisor) -> Self {
516 Self::from_parts(
517 target,
518 ChildSpecification::from(supervisor).with_restart_type(RestartType::Permanent),
519 )
520 }
521
522 /// Restarts this subtree only when it terminates abnormally.
523 ///
524 /// Narrows the restart policy to [`RestartType::Transient`], which makes
525 /// [`with_significant`][NestedSupervisorBuilder::with_significant] available.
526 pub fn transient(self) -> NestedSupervisorBuilder<'a, Terminable> {
527 self.map_spec_into(|spec| spec.with_restart_type(RestartType::Transient))
528 }
529
530 /// Never restarts this subtree.
531 ///
532 /// Narrows the restart policy to [`RestartType::Temporary`], which makes
533 /// [`with_significant`][NestedSupervisorBuilder::with_significant] available.
534 pub fn temporary(self) -> NestedSupervisorBuilder<'a, Terminable> {
535 self.map_spec_into(|spec| spec.with_restart_type(RestartType::Temporary))
536 }
537}
538
539impl<S: CanTerminate> NestedSupervisorBuilder<'_, S> {
540 /// Sets whether this subtree's termination should stop the parent supervisor.
541 ///
542 /// See [`ChildBuilder::with_significant`], which this mirrors.
543 pub fn with_significant(self, significant: bool) -> Self {
544 self.map_spec_into(|spec| spec.with_significant(significant))
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use std::sync::{
551 atomic::{AtomicUsize, Ordering},
552 Arc, Mutex,
553 };
554 use std::thread::ThreadId;
555
556 use async_trait::async_trait;
557 use saluki_common::sync::shutdown::ShutdownHandle;
558 use saluki_metrics::test::TestRecorder;
559 use tokio::sync::oneshot;
560 use tokio::time::timeout;
561
562 use super::*;
563 use crate::components::test_util::TestComponentSupervisor;
564 use crate::runtime::{InitializationError, SupervisorError, SupervisorFuture};
565 use crate::test_support::wait_until;
566
567 /// How long the drain-participating child in these tests takes to finish after its input closes.
568 ///
569 /// Long enough that a child given any short deadline of its own would be aborted before completing, so the tests
570 /// fail if children stop being bounded by the supervisor's budget alone.
571 const DRAIN_DURATION: Duration = Duration::from_millis(300);
572
573 /// A hand-written [`Supervisable`] that records where and how often it was initialized, then runs until shutdown.
574 struct CountingWorker {
575 name: &'static str,
576 initializations: Arc<AtomicUsize>,
577 thread_id: Arc<Mutex<Option<ThreadId>>>,
578 }
579
580 impl CountingWorker {
581 fn new(name: &'static str) -> (Self, Arc<AtomicUsize>, Arc<Mutex<Option<ThreadId>>>) {
582 let thread_id = Arc::new(Mutex::new(None));
583 let initializations = Arc::new(AtomicUsize::new(0));
584
585 (
586 Self {
587 name,
588 initializations: Arc::clone(&initializations),
589 thread_id: Arc::clone(&thread_id),
590 },
591 initializations,
592 thread_id,
593 )
594 }
595 }
596
597 #[async_trait]
598 impl Supervisable for CountingWorker {
599 fn name(&self) -> &str {
600 self.name
601 }
602
603 fn shutdown_strategy(&self) -> ShutdownStrategy {
604 ShutdownStrategy::Graceful(Duration::MAX)
605 }
606
607 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
608 self.initializations.fetch_add(1, Ordering::SeqCst);
609 *self.thread_id.lock().unwrap() = Some(std::thread::current().id());
610
611 Ok(Box::pin(async move {
612 process_shutdown.await;
613 Ok(())
614 }))
615 }
616 }
617
618 /// A [`Supervisable`] that fails its first run and waits for shutdown on every run after.
619 struct FailingOnceWorker {
620 initializations: Arc<AtomicUsize>,
621 }
622
623 impl FailingOnceWorker {
624 fn new() -> (Self, Arc<AtomicUsize>) {
625 let initializations = Arc::new(AtomicUsize::new(0));
626 (
627 Self {
628 initializations: Arc::clone(&initializations),
629 },
630 initializations,
631 )
632 }
633 }
634
635 #[async_trait]
636 impl Supervisable for FailingOnceWorker {
637 fn name(&self) -> &str {
638 "failing_once"
639 }
640
641 fn shutdown_strategy(&self) -> ShutdownStrategy {
642 ShutdownStrategy::Graceful(Duration::MAX)
643 }
644
645 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
646 let first_run = self.initializations.fetch_add(1, Ordering::SeqCst) == 0;
647
648 Ok(Box::pin(async move {
649 if first_run {
650 return Err(saluki_error::generic_error!("first run always fails"));
651 }
652
653 process_shutdown.await;
654 Ok(())
655 }))
656 }
657 }
658
659 #[tokio::test]
660 async fn worker_drains_to_its_terminal_condition_before_the_supervisor_stops() {
661 // The central promise of a one-shot worker: shutdown is a trigger, not an enforcement, so a worker that is
662 // still finishing when the drain starts gets to finish. A clean supervisor result is the assertion that
663 // matters -- `ShutdownTimedOut` would mean it was aborted instead.
664 let supervisor = TestComponentSupervisor::start("test_component").await;
665
666 // The child does real work *after* its input closes, so it would be cut short if children were given a
667 // near-zero deadline rather than being bounded by the supervisor's budget.
668 let drained = Arc::new(AtomicUsize::new(0));
669 let child_drained = Arc::clone(&drained);
670 let (input_tx, input_rx) = oneshot::channel::<()>();
671
672 supervisor.handle().spawn_worker("drainer", async move {
673 let _ = input_rx.await;
674 tokio::time::sleep(DRAIN_DURATION).await;
675 child_drained.fetch_add(1, Ordering::SeqCst);
676 });
677
678 supervisor.wait_for_children(1).await;
679
680 // Close the child's input, then immediately shut down while it is still draining.
681 drop(input_tx);
682 let result = supervisor.shutdown().await;
683 assert!(
684 result.is_ok(),
685 "child should have drained rather than been aborted: {result:?}"
686 );
687 assert_eq!(drained.load(Ordering::SeqCst), 1);
688 }
689
690 #[tokio::test]
691 async fn worker_without_a_terminal_condition_is_aborted_when_marked_brutal() {
692 // The counterpart: work that never ends has to say so, otherwise it holds the drain open until the budget
693 // elapses. `Brutal` is how it says so, and the supervisor still reports a clean shutdown.
694 let supervisor = TestComponentSupervisor::start_with_budget("test_component", Duration::from_secs(30)).await;
695
696 supervisor
697 .handle()
698 .worker("endless", std::future::pending::<()>())
699 .with_shutdown_strategy(ShutdownStrategy::Brutal)
700 .spawn();
701 supervisor.wait_for_children(1).await;
702
703 let started = tokio::time::Instant::now();
704 let result = supervisor.shutdown().await;
705 let elapsed = started.elapsed();
706
707 assert!(
708 result.is_ok(),
709 "an aborted brutal child is not an unclean shutdown: {result:?}"
710 );
711 assert!(
712 elapsed < Duration::from_secs(5),
713 "the child should have been aborted at once rather than held to the 30s budget; took {elapsed:?}"
714 );
715 }
716
717 #[tokio::test]
718 async fn endless_worker_is_bounded_by_the_supervisors_budget() {
719 // A one-shot child carries no deadline of its own, so the supervisor's budget is what bounds it and a stuck
720 // child is aborted when the budget elapses.
721 //
722 // This deliberately does not try to distinguish that from a child having silently fallen back to the
723 // `Supervisable` trait default of five seconds: deadlines resolve to whichever elapses first, so any budget
724 // under five seconds produces an identical result.
725 let supervisor = TestComponentSupervisor::start_with_budget("test_component", Duration::from_millis(200)).await;
726
727 supervisor.handle().spawn_worker("stuck", std::future::pending::<()>());
728 supervisor.wait_for_children(1).await;
729
730 let started = tokio::time::Instant::now();
731 let result = supervisor.shutdown().await;
732 let elapsed = started.elapsed();
733
734 assert!(
735 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
736 "the budget should have aborted the stuck child, got {result:?}"
737 );
738 assert!(
739 elapsed < Duration::from_secs(1),
740 "the child should have been bounded by the 200ms budget rather than a deadline of its own; took {elapsed:?}"
741 );
742 }
743
744 #[tokio::test]
745 async fn on_runtime_places_the_child_on_that_runtime() {
746 // `on_runtime` must actually change where the child's task runs, not just where it was spawned from.
747 let pool = tokio::runtime::Builder::new_multi_thread()
748 .worker_threads(1)
749 .thread_name("builder-pool-test")
750 .enable_all()
751 .build()
752 .expect("should build pool");
753
754 let supervisor = TestComponentSupervisor::start("test_component").await;
755
756 let (thread_tx, thread_rx) = oneshot::channel();
757 supervisor
758 .handle()
759 .worker("pooled", async move {
760 let _ = thread_tx.send(std::thread::current().name().unwrap_or_default().to_string());
761 std::future::pending::<()>().await;
762 })
763 .on_runtime(pool.handle().clone())
764 .with_shutdown_strategy(ShutdownStrategy::Brutal)
765 .spawn();
766
767 let thread_name = timeout(Duration::from_secs(5), thread_rx)
768 .await
769 .expect("child should report its thread promptly")
770 .expect("child should not be dropped before reporting");
771 assert!(
772 thread_name.starts_with("builder-pool-test"),
773 "child must run on the given runtime, but ran on thread {thread_name:?}"
774 );
775
776 assert!(supervisor.shutdown().await.is_ok());
777 pool.shutdown_background();
778 }
779
780 #[tokio::test]
781 async fn child_exiting_does_not_shut_the_supervisor_down() {
782 // Children are non-significant, so one finishing -- the normal case during a drain -- must not trip the
783 // supervisor's `AutoShutdown::AnySignificant` policy and tear the component down with it.
784 let supervisor = TestComponentSupervisor::start("test_component").await;
785 let handle = supervisor.handle();
786
787 // Wait for the child to have actually run and exited. `wait_for_children(0)` would be vacuously true before
788 // the supervisor ever picked it up, so the exit this test is about would never be observed.
789 let exited = Arc::new(AtomicUsize::new(0));
790 let child_exited = Arc::clone(&exited);
791 handle.spawn_worker("brief", async move {
792 child_exited.fetch_add(1, Ordering::SeqCst);
793 });
794 wait_until("the child has run and exited", || exited.load(Ordering::SeqCst) == 1).await;
795 supervisor.wait_for_children(0).await;
796
797 // The supervisor survived that exit: it is still running and still accepting work.
798 assert!(handle.is_running());
799 handle
800 .worker("second", std::future::pending::<()>())
801 .with_shutdown_strategy(ShutdownStrategy::Brutal)
802 .spawn();
803
804 assert!(supervisor.shutdown().await.is_ok());
805 }
806
807 #[tokio::test]
808 async fn a_significant_child_exiting_stops_the_supervisor() {
809 // The counterpart: a component supervisor uses `AutoShutdown::AnySignificant`, so a child marked significant
810 // takes the component with it when it terminates -- here on a perfectly clean exit, which is the part that
811 // surprises.
812 let supervisor = TestComponentSupervisor::start("test_component").await;
813 let handle = supervisor.handle();
814
815 handle.worker("brief", async {}).with_significant(true).spawn();
816
817 // Spawning only queues the child, so wait for the supervisor to actually stop itself rather than racing our
818 // own shutdown against the child ever starting.
819 wait_until("the supervisor has stopped", || !handle.is_running()).await;
820
821 let result = supervisor.shutdown().await;
822 assert!(
823 matches!(result, Err(SupervisorError::SignificantChildExited)),
824 "a significant child's exit should have stopped the supervisor, got {result:?}"
825 );
826 }
827
828 #[tokio::test]
829 async fn spawned_children_record_poll_metrics() {
830 // Every supervised worker's task is timed, and a child spawned through the builder is no exception. The tag is
831 // the child's fully qualified process name, which is what gives one series per name rather than per task.
832 let recorder = TestRecorder::default();
833 let _guard = metrics::set_default_local_recorder(&recorder);
834
835 // The recorder must be installed before the child is spawned: metric handles are resolved once, at spawn.
836 let supervisor = TestComponentSupervisor::start("metrics_component").await;
837
838 // Spawning only queues the child, so wait for it to actually run before shutting down -- otherwise there's no
839 // guarantee it was ever polled, and therefore none that it recorded anything.
840 let (ran_tx, ran_rx) = oneshot::channel();
841 supervisor.handle().spawn_worker("instrumented", async move {
842 let _ = ran_tx.send(());
843 });
844 ran_rx.await.expect("child should have run");
845
846 assert!(supervisor.shutdown().await.is_ok());
847
848 let polls = recorder.counter((
849 "runtime_task_poll_count",
850 &[("task_name", "metrics_component.instrumented")],
851 ));
852 assert!(
853 polls.is_some_and(|polls| polls > 0),
854 "spawned child should have recorded poll metrics, got {polls:?}"
855 );
856 }
857
858 #[tokio::test]
859 async fn supervisable_child_can_be_configured_before_spawning() {
860 // A `Supervisable` child goes through the same builder, including runtime placement.
861 let worker_pool_thread_id = Arc::new(Mutex::new(None));
862 let worker_pool_thread_id2 = Arc::clone(&worker_pool_thread_id);
863
864 let pool = tokio::runtime::Builder::new_multi_thread()
865 .worker_threads(1)
866 .on_thread_start(move || {
867 worker_pool_thread_id2
868 .lock()
869 .unwrap()
870 .replace(std::thread::current().id());
871 })
872 .enable_all()
873 .build()
874 .expect("should build pool");
875
876 let supervisor = TestComponentSupervisor::start("test_component").await;
877
878 let (worker, initializations, worker_thread_id) = CountingWorker::new("counting");
879 supervisor
880 .handle()
881 .supervisable(worker)
882 .on_runtime(pool.handle().clone())
883 .spawn();
884
885 // `spawn` returns once the child is queued, and `initialize` runs inside the child's own task -- on the pool's
886 // runtime here -- so wait for it rather than assuming it has been polled.
887 supervisor.wait_for_children(1).await;
888 wait_until("the worker has initialized once", || {
889 initializations.load(Ordering::SeqCst) == 1
890 })
891 .await;
892
893 // Assert where it actually ran, not just that it ran: without this, `on_runtime` could be a no-op and the test
894 // would still pass.
895 let worker_pool_thread_id = worker_pool_thread_id
896 .lock()
897 .unwrap()
898 .expect("worker pool thread should have recorded its thread ID");
899 let worker_thread_id = worker_thread_id
900 .lock()
901 .unwrap()
902 .expect("worker should have recorded its thread ID");
903 assert_eq!(
904 worker_pool_thread_id, worker_thread_id,
905 "child should have run on the given runtime"
906 );
907
908 assert!(supervisor.shutdown().await.is_ok());
909 pool.shutdown_background();
910 }
911
912 #[tokio::test]
913 async fn supervisable_children_are_restarted_by_default() {
914 let supervisor = TestComponentSupervisor::start("test_component").await;
915
916 let (worker, initializations) = FailingOnceWorker::new();
917 supervisor.handle().spawn_supervisable(worker);
918
919 // The worker fails its first run, but then runs forever after that, so we should observe two initializations
920 // and no more after that.
921 wait_until("the worker has initialized twice", || {
922 initializations.load(Ordering::SeqCst) == 2
923 })
924 .await;
925 assert!(supervisor.shutdown().await.is_ok());
926 }
927
928 #[tokio::test]
929 async fn spawning_after_shutdown_never_starts_the_child() {
930 let supervisor = TestComponentSupervisor::start("test_component").await;
931 let handle = supervisor.handle();
932 assert!(supervisor.shutdown().await.is_ok());
933
934 let started = Arc::new(AtomicUsize::new(0));
935 let child_started = Arc::clone(&started);
936 handle.spawn_worker("late", async move {
937 child_started.fetch_add(1, Ordering::SeqCst);
938 });
939
940 tokio::time::sleep(Duration::from_millis(50)).await;
941 assert_eq!(
942 started.load(Ordering::SeqCst),
943 0,
944 "a child spawned against a stopped supervisor must never run"
945 );
946 }
947}