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//! The builder has an ambient form and an explicit form, one for each of the two ways to spawn. The free functions in
9//! this module ([`worker`], [`supervisable`]) spawn into the current process. The methods with the same names on
10//! [`SupervisorHandle`] spawn onto the supervisor of that handle. In both forms, [`ChildBuilder::spawn`] starts the
11//! child. [`ChildBuilder::build`] gives the child to [`Supervisor::add_worker`][add_worker], and the supervisor starts
12//! the child when it runs.
13//!
14//! [add_worker]: super::Supervisor::add_worker
15//!
16//! # Restart policy
17//!
18//! You do not choose the restart policy of a child directly. The policy follows from two facts: whether the child can
19//! be started again, and whether its owner can work without it. See the [runtime documentation][restart] for how the
20//! policy is derived, and for the defaults.
21//!
22//! [restart]: super#restart-policy
23//!
24//! # Shutdown
25//!
26//! A subtree shutdown is a _trigger_, not an enforcement. A one-shot child built with [`worker`] never receives the
27//! shutdown signal. It runs until it reaches its own terminal condition, and its owner waits for it. Examples of a
28//! terminal condition are an input channel that closes, a loop that finishes, and a request that completes.
29//!
30//! This behavior lets a set of tasks that are connected by channels drain in dependency order. The tasks do not have to
31//! know that order. Because of this behavior, it is not a problem that a one-shot child cannot see the shutdown signal.
32//!
33//! Each child is held to its own shutdown strategy, whether its owner is a supervisor or a scope. A closure-based child
34//! is graceful, with a timeout of five seconds. When a timeout expires, the process is halted along with everything in
35//! its scope.
36//!
37//! Two cases need something other than the default:
38//!
39//! - Work with no terminal condition holds the drain open until its timeout expires. An endless background loop is an
40//! example. Give it [`ShutdownStrategy::Brutal`] with [`ChildBuilder::with_shutdown_strategy`] so that it is halted
41//! at once.
42//! - If work must observe shutdown to know that it must stop, or must run cleanup, build it with
43//! [`worker_with_shutdown`]. The body of that worker receives the shutdown signal. You can also implement
44//! [`Supervisable`] directly.
45//!
46//! [`ChildBuilder::with_shutdown_timeout`] sets a timeout other than the default. Use it if a particular child needs
47//! more time to drain, or must be halted sooner than its siblings.
48//!
49//! # Children of the current process
50//!
51//! [`ChildBuilder::spawn`] makes a child a child of the current process, and the [scope][super::scope] of that process
52//! holds it. The process does not finish until its children finish. When the body of the process returns, the scope
53//! stops the children, each according to its own shutdown strategy. [`ChildBuilder::spawn_sibling`] gives the child
54//! to the nearest supervisor above the current process instead.
55//!
56//! # Task naming
57//!
58//! Child task names should generally _not_ contain unique patterns/tokens -- such as monotonic IDs or high-cardinality
59//! values -- as they are used for internal telemetry about the task. Generally, task names should be thought of as a
60//! category label: if a supervisor manages tasks for handling connections, it should prefer to name them like
61//! `conn_handler` instead of `conn_handler_<ID or IP>`.
62
63use std::{future::Future, marker::PhantomData, time::Duration};
64
65use saluki_common::sync::shutdown::ShutdownHandle;
66use tokio::runtime::Handle;
67
68#[cfg(any(test, feature = "test-util"))]
69use super::ExitObserver;
70use super::{
71 scope::{self, Edge, Scope, UnadoptedScope},
72 ChildId, ChildSpecification, FnWorker, IntoWorkerResult, RestartType, ShutdownStrategy, Supervisable, Supervisor,
73 SupervisorHandle, SupervisorSpec, WorkerSpec,
74};
75
76/// Creates a builder for a child task of the current process.
77///
78/// The ambient counterpart to [`SupervisorHandle::worker`].
79///
80/// # Panics
81///
82/// [`ChildBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
83pub fn worker<N, Fut>(name: N, fut: Fut) -> ChildBuilder<'static>
84where
85 N: Into<String>,
86 Fut: Future + Send + 'static,
87 Fut::Output: IntoWorkerResult,
88{
89 ChildBuilder::new_one_shot(BuilderTarget::Ambient, FnWorker::new(name, fut))
90}
91
92/// Creates a builder for a child task of the current process, with a body that receives the shutdown signal.
93///
94/// The ambient counterpart to [`SupervisorHandle::worker_with_shutdown`].
95///
96/// # Panics
97///
98/// [`ChildBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
99pub fn worker_with_shutdown<N, F, Fut>(name: N, f: F) -> ChildBuilder<'static>
100where
101 N: Into<String>,
102 F: FnOnce(ShutdownHandle) -> Fut + Send + 'static,
103 Fut: Future + Send + 'static,
104 Fut::Output: IntoWorkerResult,
105{
106 ChildBuilder::new_one_shot(BuilderTarget::Ambient, FnWorker::with_shutdown(name, f))
107}
108
109/// Creates a builder for a supervisable child task of the current process.
110///
111/// The ambient counterpart to [`SupervisorHandle::supervisable`], which documents what a supervisable task is.
112///
113/// # Panics
114///
115/// [`ChildBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
116pub fn supervisable<T>(worker: T) -> ChildBuilder<'static, Restartable>
117where
118 T: Supervisable + 'static,
119{
120 ChildBuilder::new_restartable(BuilderTarget::Ambient, worker)
121}
122
123impl SupervisorHandle {
124 /// Creates a builder for a child task built from a plain future.
125 ///
126 /// The task runs until it reaches its own terminal condition; it is never handed the shutdown signal. See
127 /// [`FnWorker`] for what that means at shutdown, and for the two cases that need something else.
128 ///
129 /// Use this method when advanced configuration of the underlying task is required. Otherwise, prefer
130 /// [`spawn_worker`][Self::spawn_worker].
131 ///
132 /// # Examples
133 ///
134 /// ```no_run
135 /// # use saluki_core::runtime::SupervisorHandle;
136 /// # async fn encode() {}
137 /// # fn example(supervisor: &SupervisorHandle, pool: tokio::runtime::Handle) {
138 /// supervisor.worker("encoder", encode()).on_runtime(pool).spawn();
139 /// # }
140 /// ```
141 pub fn worker<N, Fut>(&self, name: N, fut: Fut) -> ChildBuilder<'_>
142 where
143 N: Into<String>,
144 Fut: Future + Send + 'static,
145 Fut::Output: IntoWorkerResult,
146 {
147 ChildBuilder::new_one_shot(BuilderTarget::Handle(self), FnWorker::new(name, fut))
148 }
149
150 /// Creates a builder for a child task with a body that receives the shutdown signal.
151 ///
152 /// `f` receives the [`ShutdownHandle`] of the task when the task starts. Use this method for work that must observe
153 /// shutdown to stop in time. Such work stops when it observes shutdown, not when it reaches its own terminal
154 /// condition. An example is a long-lived connection that must close gracefully. See [`FnWorker::with_shutdown`].
155 pub fn worker_with_shutdown<N, F, Fut>(&self, name: N, f: F) -> ChildBuilder<'_>
156 where
157 N: Into<String>,
158 F: FnOnce(ShutdownHandle) -> Fut + Send + 'static,
159 Fut: Future + Send + 'static,
160 Fut::Output: IntoWorkerResult,
161 {
162 ChildBuilder::new_one_shot(BuilderTarget::Handle(self), FnWorker::with_shutdown(name, f))
163 }
164
165 /// Creates a builder for a supervisable child task.
166 ///
167 /// Supervisable tasks are those where the worker already implements [`Supervisable`], which lets the builder serve
168 /// as a consistent control surface for spawning both arbitrary asynchronous functions and more full-fledged
169 /// workers.
170 ///
171 /// A supervisable task can usually be started again, so its owner restarts it when it fails. A task that reports
172 /// through [`can_restart`][Supervisable::can_restart] that it cannot be started again is never restarted. See the
173 /// [runtime documentation][super#restart-policy] for how its restart policy is decided.
174 ///
175 /// Use this method when advanced configuration of the underlying task is required. Otherwise, prefer
176 /// [`spawn_supervisable`][Self::spawn_supervisable].
177 pub fn supervisable<T>(&self, worker: T) -> ChildBuilder<'_, Restartable>
178 where
179 T: Supervisable + 'static,
180 {
181 ChildBuilder::new_restartable(BuilderTarget::Handle(self), worker)
182 }
183
184 /// Spawns a child task built from a plain future.
185 ///
186 /// The task runs until it reaches its own terminal condition; it is never handed the shutdown signal. See
187 /// [`FnWorker`] for what that means at shutdown, and for the two cases that need something else.
188 ///
189 /// Use [`worker`][Self::worker] when advanced configuration of the underlying task is required.
190 pub fn spawn_worker<N, Fut>(&self, name: N, fut: Fut) -> ChildId
191 where
192 N: Into<String>,
193 Fut: Future + Send + 'static,
194 Fut::Output: IntoWorkerResult,
195 {
196 self.worker(name, fut).spawn()
197 }
198
199 /// Spawns a supervisable child task.
200 ///
201 /// Supervisable tasks are those where the worker already implements [`Supervisable`], which lets the builder serve
202 /// as a consistent control surface for spawning both arbitrary asynchronous functions and more full-fledged
203 /// workers.
204 ///
205 /// Use [`supervisable`][Self::supervisable] when advanced configuration of the underlying task is required.
206 pub fn spawn_supervisable<T>(&self, worker: T) -> ChildId
207 where
208 T: Supervisable + 'static,
209 {
210 self.supervisable(worker).spawn()
211 }
212
213 /// Creates a builder for a nested supervisor.
214 ///
215 /// You can give a [`Supervisor`] directly to [`spawn`][Self::spawn] or [`Supervisor::add_worker`]. Most callers
216 /// need only that. This builder is for the settings that are not available in that way: whether the subtree is
217 /// needed or hosted, and whether it [stops with its owner][NestedSupervisorBuilder::stops_with_owner] in a scope.
218 ///
219 /// Unlike [`supervisable`][Self::supervisable], there is no placement or shutdown setting. A nested supervisor runs
220 /// wherever its parent does and its children carry their own placement, and its drain is bounded by its children's
221 /// timeouts and by its owner.
222 pub fn nested_supervisor(&self, supervisor: Supervisor) -> NestedSupervisorBuilder<'_> {
223 NestedSupervisorBuilder::new(BuilderTarget::Handle(self), supervisor)
224 }
225}
226
227/// Creates a builder for a nested supervisor spawned into the current process.
228///
229/// The ambient counterpart to [`SupervisorHandle::nested_supervisor`], which documents what the builder is for.
230///
231/// # Panics
232///
233/// [`NestedSupervisorBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
234pub fn nested_supervisor(supervisor: Supervisor) -> NestedSupervisorBuilder<'static> {
235 NestedSupervisorBuilder::new(BuilderTarget::Ambient, supervisor)
236}
237
238mod sealed {
239 pub trait Sealed {}
240}
241
242/// The kind of worker a [`ChildBuilder`] is describing.
243///
244/// This trait is sealed. It exists only to mark which configuration a builder makes available. [`OneShot`] and
245/// [`Restartable`] implement it.
246pub trait BuilderState: sealed::Sealed {}
247
248/// Marks a builder whose worker can only be started once.
249///
250/// Closure-based children ([`SupervisorHandle::worker`]) consume their body when they start, so they can never be
251/// restarted.
252pub struct OneShot;
253
254/// Marks a builder whose worker can usually be started more than once.
255///
256/// A [`Supervisable`] builds its work in [`initialize`][Supervisable::initialize] each time it starts, so it can be
257/// restarted. A [`Supervisable`] that consumes something when it starts says so through
258/// [`can_restart`][Supervisable::can_restart], and its owner never restarts it.
259pub struct Restartable;
260
261impl sealed::Sealed for OneShot {}
262impl BuilderState for OneShot {}
263impl sealed::Sealed for Restartable {}
264impl BuilderState for Restartable {}
265
266/// Builder for a yet-to-be-started child task.
267///
268/// This is the only way to configure a child: [`ChildSpecification`], which this produces, carries no settings of its
269/// own. The builder uses the typestate pattern to expose only the properties that apply to the child that it describes.
270/// See [`BuilderState`] for the states.
271#[must_use = "a child is only described until `spawn` or `build` is called"]
272pub struct ChildBuilder<'a, S = OneShot> {
273 target: BuilderTarget<'a>,
274 spec: ChildSpecification<WorkerSpec>,
275 _state: PhantomData<S>,
276}
277
278/// The place where a [`ChildBuilder`] spawns its child.
279#[derive(Clone, Copy)]
280enum BuilderTarget<'a> {
281 /// The scope of the current process, or the ambient supervisor if there is no current scope.
282 Ambient,
283
284 /// This specific supervisor.
285 Handle(&'a SupervisorHandle),
286}
287
288impl<'a, S: BuilderState> ChildBuilder<'a, S> {
289 fn new(target: BuilderTarget<'a>, spec: ChildSpecification<WorkerSpec>) -> Self {
290 Self {
291 target,
292 spec,
293 _state: PhantomData,
294 }
295 }
296
297 fn map_spec<F>(self, f: F) -> Self
298 where
299 F: FnOnce(ChildSpecification<WorkerSpec>) -> ChildSpecification<WorkerSpec>,
300 {
301 let Self { target, spec, .. } = self;
302
303 Self::new(target, f(spec))
304 }
305
306 /// Runs this child task on a specific runtime.
307 ///
308 /// Use this method for compute-heavy work that must not compete with the runtime that drives its owner and its I/O.
309 /// Examples are encoding, serialization, and protocol servers. This method changes only where the task runs. The
310 /// owner of the child still supervises, shuts down, and restarts the child.
311 pub fn on_runtime(self, handle: Handle) -> Self {
312 self.map_spec(|spec| spec.with_runtime(handle))
313 }
314
315 /// Sets an explicit shutdown timeout for this child task.
316 ///
317 /// A closure-based child defaults to five seconds, after which it is halted. Set this if a particular child needs
318 /// more time to drain, or must be halted sooner. A timeout longer than its owner's has no effect, since the child
319 /// is halted along with its owner.
320 ///
321 /// For a [`Supervisable`] child, this overrides the strategy the task reports for itself.
322 pub fn with_shutdown_timeout(self, timeout: Duration) -> Self {
323 self.with_shutdown_strategy(ShutdownStrategy::Graceful(timeout))
324 }
325
326 /// Sets the explicit shutdown strategy used for this child task.
327 pub fn with_shutdown_strategy(self, strategy: ShutdownStrategy) -> Self {
328 self.map_spec(|spec| spec.with_shutdown_strategy(strategy))
329 }
330
331 /// Marks this child as one that its owner cannot work without.
332 ///
333 /// If the child stops without a request to stop and can only be started once, its owner also stops and fails. This
334 /// occurs even when the child exits cleanly. Thus, use this method for a child that its owner cannot work without.
335 /// Do not use it for a child that you expect to finish by itself. If the child can be started again, its owner
336 /// restarts it after any such stop, not only after a failure.
337 ///
338 /// For example, one failed connection is usually not a reason to stop a component that handles client connections.
339 /// But a component that forwards work to a child task can become inoperable if that task stops unexpectedly. This
340 /// occurs if the task cannot be attached again to the necessary channels or state unless the component is created
341 /// again.
342 ///
343 /// See the [runtime documentation][super#restart-policy] for the default.
344 pub fn needed(self) -> Self {
345 self.map_spec(|spec| spec.with_edge(Edge::Needed))
346 }
347
348 /// Marks this child as one that its owner runs but does not need.
349 ///
350 /// If the child stops without a request to stop, and its owner does not restart it, the owner reaps the child and
351 /// continues. The owner does not restart a child that can only be started once, or a child that exited cleanly. If
352 /// the child can be started again, its owner restarts it only if it failed, and accepts a clean exit as final. But
353 /// if the child fails more often than the restart limit of its owner allows, the owner also fails, as for a needed
354 /// child.
355 ///
356 /// See the [runtime documentation][super#restart-policy] for the default.
357 pub fn hosted(self) -> Self {
358 self.map_spec(|spec| spec.with_edge(Edge::Hosted))
359 }
360
361 /// Sets whether the supervisor stops when this child task terminates.
362 ///
363 /// This setting has an effect only under a supervisor that uses the deprecated
364 /// [`with_auto_shutdown`][super::Supervisor::with_auto_shutdown]. It also has no effect on a child that its
365 /// supervisor always restarts, such as a child that can start again and that you declare with
366 /// [`Supervisor::add_worker`] without marking it [hosted][Self::hosted]. The supervisor logs a warning for each
367 /// child where the setting has no effect.
368 #[deprecated(note = "mark the child `needed` instead, which stops its owner when it terminates for good")]
369 pub fn with_significant(self, significant: bool) -> Self {
370 self.map_spec(|spec| spec.with_significant(significant))
371 }
372
373 /// Makes this child adopt `scope` as its own and start what was spawned into it before the child started.
374 pub(crate) fn adopting(self, scope: UnadoptedScope) -> Self {
375 self.map_spec(|spec| spec.adopting(scope))
376 }
377
378 /// Gives this child a resource group of its own, with the same name as the child.
379 ///
380 /// The child then does not count against the resource group of its owner. Use this for a worker that represents
381 /// something with its own resource accounting, such as a topology component.
382 pub(crate) fn with_own_resource_group(self) -> Self {
383 self.map_spec(|spec| spec.with_own_resource_group())
384 }
385
386 /// Makes `observer` receive how this child ended, after its scope closed. See [`ExitObserver`].
387 #[cfg(any(test, feature = "test-util"))]
388 pub(crate) fn observing_exit(self, observer: ExitObserver) -> Self {
389 self.map_spec(|spec| spec.observing_exit(observer))
390 }
391
392 /// Finishes describing the child without starting it, for [`Supervisor::add_worker`].
393 ///
394 /// Use this method to register a configured child on a supervisor that has not started yet. The supervisor starts
395 /// this child when it runs. When the supervisor restarts, the child restarts with it. To start a child now, use
396 /// [`spawn`][Self::spawn].
397 ///
398 /// Whichever supervisor this builder was created against is irrelevant here -- the child belongs to whichever one
399 /// it is handed to.
400 ///
401 /// [`Supervisor::add_worker`]: super::Supervisor::add_worker
402 pub fn build(self) -> ChildSpecification<WorkerSpec> {
403 self.spec
404 }
405
406 /// Spawns the child.
407 ///
408 /// A builder that was created from a [`SupervisorHandle`] spawns the child onto that supervisor. Other builders
409 /// make the child a child of the current process, and the [scope][super::scope] of that process holds it. See
410 /// [`spawn`][super::spawn].
411 ///
412 /// Returns the [`ChildId`] of the child. This method queues the child and does not start it synchronously. Thus,
413 /// when this method returns, the child can still be in the queue. If its owner does not run, or stops before it
414 /// processes the child, the child never runs.
415 ///
416 /// # Panics
417 ///
418 /// Panics if this builder does not target a specific supervisor and code outside supervision calls this method. See
419 /// [`spawn`][super::spawn].
420 pub fn spawn(self) -> ChildId {
421 let Self { target, spec, .. } = self;
422
423 match target {
424 BuilderTarget::Ambient => super::spawn(spec),
425 BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
426 }
427 }
428
429 /// Spawns the child onto the ambient supervisor, the nearest supervisor above the current process.
430 ///
431 /// The child is a sibling of the current process only if a supervisor runs that process directly. For a process
432 /// that runs in a scope, the child goes to the nearest supervisor above it instead. See
433 /// [`spawn_sibling`][super::spawn_sibling].
434 ///
435 /// A builder that was created from a [`SupervisorHandle`] spawns the child onto that supervisor, exactly as
436 /// [`spawn`][Self::spawn] does.
437 ///
438 /// # Panics
439 ///
440 /// Panics if this builder does not target a specific supervisor and there is no ambient supervisor.
441 pub fn spawn_sibling(self) -> ChildId {
442 let Self { target, spec, .. } = self;
443
444 match target {
445 BuilderTarget::Ambient => super::spawn_sibling(spec),
446 BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
447 }
448 }
449
450 /// Spawns the child into `scope`.
451 pub fn spawn_in(self, scope: &Scope) -> ChildId {
452 let Self { spec, .. } = self;
453
454 scope.spawn(spec)
455 }
456
457 /// Spawns the child into the scope of the current process, or as a detached task outside supervision.
458 ///
459 /// Returns the [`ChildId`] of the child, or `None` if there was no current scope and the child started as a
460 /// detached task instead. A detached child never receives a shutdown signal, and nothing restarts it or waits for
461 /// it. This is how the same work runs without supervision at all. Thus, this method is the correct choice for code
462 /// that can be called from any location, such as a primitive that a test can construct.
463 pub fn spawn_or_detached(self) -> Option<ChildId> {
464 let Self { spec, .. } = self;
465
466 match scope::current() {
467 Some(scope) => Some(scope.spawn(spec)),
468 None => {
469 scope::spawn_detached(spec, ShutdownHandle::noop());
470 None
471 }
472 }
473 }
474}
475
476impl<'a> ChildBuilder<'a, OneShot> {
477 fn new_one_shot<T>(target: BuilderTarget<'a>, worker: T) -> Self
478 where
479 T: Supervisable + 'static,
480 {
481 let spec = ChildSpecification::worker(worker);
482
483 Self::new(target, spec)
484 }
485}
486
487impl<'a> ChildBuilder<'a, Restartable> {
488 fn new_restartable<T>(target: BuilderTarget<'a>, worker: T) -> Self
489 where
490 T: Supervisable + 'static,
491 {
492 Self::new(target, ChildSpecification::worker(worker))
493 }
494
495 /// Restarts this child task only when it terminates abnormally.
496 #[deprecated(note = "mark the child `hosted` instead, which restarts it only if it fails")]
497 pub fn transient(self) -> Self {
498 self.map_spec(|spec| spec.with_restart_type(RestartType::Transient))
499 }
500
501 /// Never restarts this child task.
502 #[deprecated(note = "implement `Supervisable::can_restart` instead, if the child can only be started once")]
503 pub fn temporary(self) -> Self {
504 self.map_spec(|spec| spec.with_restart_type(RestartType::Temporary))
505 }
506}
507
508/// Builder for a yet-to-be-started nested supervisor.
509///
510/// The counterpart to [`ChildBuilder`] for a child that is itself a [`Supervisor`], and deliberately a much smaller
511/// surface. A nested supervisor has no placement of its own, because it runs where its parent runs and its children
512/// carry their own placement. It also has no shutdown deadline of its own, because it limits its own drain through its
513/// children. Thus, the settings that remain are whether it is needed or hosted, and whether it stops with its owner.
514#[must_use = "a child is only described until `spawn` or `build` is called"]
515pub struct NestedSupervisorBuilder<'a> {
516 target: BuilderTarget<'a>,
517 spec: ChildSpecification<SupervisorSpec>,
518}
519
520impl<'a> NestedSupervisorBuilder<'a> {
521 fn new(target: BuilderTarget<'a>, supervisor: Supervisor) -> Self {
522 Self {
523 target,
524 spec: ChildSpecification::from(supervisor),
525 }
526 }
527
528 fn map_spec<F>(self, f: F) -> Self
529 where
530 F: FnOnce(ChildSpecification<SupervisorSpec>) -> ChildSpecification<SupervisorSpec>,
531 {
532 let Self { target, spec } = self;
533
534 Self { target, spec: f(spec) }
535 }
536
537 /// Marks this subtree as one that its owner cannot work without.
538 ///
539 /// See [`ChildBuilder::needed`], which this method mirrors.
540 pub fn needed(self) -> Self {
541 self.map_spec(|spec| spec.with_edge(Edge::Needed))
542 }
543
544 /// Marks this subtree as one that its owner runs but does not need.
545 ///
546 /// See [`ChildBuilder::hosted`], which this method mirrors.
547 pub fn hosted(self) -> Self {
548 self.map_spec(|spec| spec.with_edge(Edge::Hosted))
549 }
550
551 /// Tells this subtree to stop as soon as its owner is told to stop, instead of when the owner's scope closes.
552 ///
553 /// Use this for ingress whose output the owner's body consumes, such as a server whose handlers feed a channel that
554 /// the body drains. The server stops accepting work and finishes what it accepted while the body keeps draining.
555 ///
556 /// This takes effect only in the scope of a worker that takes the shutdown signal (see
557 /// [`Supervisable::wants_shutdown_signal`]). A supervisor tells all of its children to stop at once anyway, and a
558 /// worker that takes no signal has nothing to stop the subtree with.
559 ///
560 /// The scope keeps the subtree's specification until the subtree exits, and the specification holds whatever the
561 /// subtree was built with, such as the sender in a request handler. Thus, the channel closes once the subtree exits.
562 /// The body **MUST NOT** hold a sender of that channel itself, or the channel never closes. See the
563 /// [scope documentation][super::scope#nested-supervisors-that-stop-with-their-owner].
564 pub fn stops_with_owner(self) -> Self {
565 self.map_spec(|spec| spec.stopping_with_owner())
566 }
567
568 /// Restarts this subtree only when it terminates abnormally.
569 #[deprecated(note = "mark the subtree `hosted` instead, which restarts it only if it fails")]
570 pub fn transient(self) -> Self {
571 self.map_spec(|spec| spec.with_restart_type(RestartType::Transient))
572 }
573
574 /// Never restarts this subtree.
575 #[deprecated(note = "a supervisor can always be started again; mark it `hosted` or `needed` instead")]
576 pub fn temporary(self) -> Self {
577 self.map_spec(|spec| spec.with_restart_type(RestartType::Temporary))
578 }
579
580 /// Sets whether the parent supervisor stops when this subtree terminates.
581 #[deprecated(note = "mark the subtree `needed` instead, which stops its owner when it terminates for good")]
582 pub fn with_significant(self, significant: bool) -> Self {
583 self.map_spec(|spec| spec.with_significant(significant))
584 }
585
586 /// Finishes describing the child without starting it, for [`Supervisor::add_worker`].
587 ///
588 /// Use this method to register a configured subtree on a supervisor that has not started yet. To start the subtree
589 /// now, use [`spawn`][Self::spawn].
590 ///
591 /// Whichever supervisor this builder was created against is irrelevant here -- the child belongs to whichever one
592 /// it is handed to.
593 pub fn build(self) -> ChildSpecification<SupervisorSpec> {
594 self.spec
595 }
596
597 /// Spawns the nested supervisor.
598 ///
599 /// This method works as [`ChildBuilder::spawn`] does. It spawns onto the target supervisor if the builder has one.
600 /// If not, it spawns the nested supervisor as a child of the current process.
601 ///
602 /// # Panics
603 ///
604 /// Panics if this builder does not target a specific supervisor and code outside supervision calls this method. See
605 /// [`spawn`][super::spawn].
606 pub fn spawn(self) -> ChildId {
607 let Self { target, spec } = self;
608
609 match target {
610 BuilderTarget::Ambient => super::spawn(spec),
611 BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
612 }
613 }
614
615 /// Spawns the nested supervisor onto the ambient supervisor, the nearest supervisor above the current process.
616 ///
617 /// This method works as [`ChildBuilder::spawn_sibling`] does.
618 ///
619 /// # Panics
620 ///
621 /// Panics if this builder does not target a specific supervisor and there is no ambient supervisor.
622 pub fn spawn_sibling(self) -> ChildId {
623 let Self { target, spec } = self;
624
625 match target {
626 BuilderTarget::Ambient => super::spawn_sibling(spec),
627 BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
628 }
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use std::sync::{
635 atomic::{AtomicUsize, Ordering},
636 Arc, Mutex,
637 };
638 use std::thread::ThreadId;
639
640 use async_trait::async_trait;
641 use saluki_common::sync::shutdown::ShutdownHandle;
642 use saluki_metrics::test::TestRecorder;
643 use tokio::sync::oneshot;
644 use tokio::time::timeout;
645
646 use super::*;
647 use crate::runtime::test_util::TestSupervisor;
648 use crate::runtime::{InitializationError, SupervisorError, SupervisorFuture};
649 use crate::test_support::wait_until;
650
651 /// How long the drain-participating child in these tests takes to finish after its input closes.
652 ///
653 /// Long enough to tell a completed drain apart from a halt.
654 const DRAIN_DURATION: Duration = Duration::from_millis(300);
655
656 /// A hand-written [`Supervisable`] that records where and how often it was initialized, then runs until shutdown.
657 struct CountingWorker {
658 name: &'static str,
659 initializations: Arc<AtomicUsize>,
660 thread_id: Arc<Mutex<Option<ThreadId>>>,
661 }
662
663 impl CountingWorker {
664 fn new(name: &'static str) -> (Self, Arc<AtomicUsize>, Arc<Mutex<Option<ThreadId>>>) {
665 let thread_id = Arc::new(Mutex::new(None));
666 let initializations = Arc::new(AtomicUsize::new(0));
667
668 (
669 Self {
670 name,
671 initializations: Arc::clone(&initializations),
672 thread_id: Arc::clone(&thread_id),
673 },
674 initializations,
675 thread_id,
676 )
677 }
678 }
679
680 #[async_trait]
681 impl Supervisable for CountingWorker {
682 fn name(&self) -> &str {
683 self.name
684 }
685
686 fn shutdown_strategy(&self) -> ShutdownStrategy {
687 ShutdownStrategy::Graceful(Duration::MAX)
688 }
689
690 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
691 self.initializations.fetch_add(1, Ordering::SeqCst);
692 *self.thread_id.lock().unwrap() = Some(std::thread::current().id());
693
694 Ok(Box::pin(async move {
695 process_shutdown.await;
696 Ok(())
697 }))
698 }
699 }
700
701 /// A [`Supervisable`] that fails its first run and waits for shutdown on every run after.
702 struct FailingOnceWorker {
703 initializations: Arc<AtomicUsize>,
704 restartable: bool,
705 }
706
707 impl FailingOnceWorker {
708 fn new() -> (Self, Arc<AtomicUsize>) {
709 let initializations = Arc::new(AtomicUsize::new(0));
710 (
711 Self {
712 initializations: Arc::clone(&initializations),
713 restartable: true,
714 },
715 initializations,
716 )
717 }
718
719 /// Makes this worker report that it cannot be started again, as a worker that consumes its state does.
720 fn one_shot(mut self) -> Self {
721 self.restartable = false;
722 self
723 }
724 }
725
726 #[async_trait]
727 impl Supervisable for FailingOnceWorker {
728 fn name(&self) -> &str {
729 "failing_once"
730 }
731
732 fn shutdown_strategy(&self) -> ShutdownStrategy {
733 ShutdownStrategy::Graceful(Duration::MAX)
734 }
735
736 fn can_restart(&self) -> bool {
737 self.restartable
738 }
739
740 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
741 let first_run = self.initializations.fetch_add(1, Ordering::SeqCst) == 0;
742
743 Ok(Box::pin(async move {
744 if first_run {
745 return Err(saluki_error::generic_error!("first run always fails"));
746 }
747
748 process_shutdown.await;
749 Ok(())
750 }))
751 }
752 }
753
754 #[tokio::test]
755 async fn worker_drains_to_its_terminal_condition_before_the_supervisor_stops() {
756 // The central promise of a one-shot worker: shutdown is a trigger, not an enforcement, so a worker that is
757 // still finishing when the drain starts gets to finish. A clean supervisor result is the assertion that
758 // matters -- `ShutdownTimedOut` would mean it was aborted instead.
759 let supervisor = TestSupervisor::start("test_component").await;
760
761 // The child does real work *after* its input closes, which a premature halt would cut short.
762 let drained = Arc::new(AtomicUsize::new(0));
763 let child_drained = Arc::clone(&drained);
764 let (input_tx, input_rx) = oneshot::channel::<()>();
765
766 supervisor.handle().spawn_worker("drainer", async move {
767 let _ = input_rx.await;
768 tokio::time::sleep(DRAIN_DURATION).await;
769 child_drained.fetch_add(1, Ordering::SeqCst);
770 });
771
772 supervisor.wait_for_children(1).await;
773
774 // Close the child's input, then immediately shut down while it is still draining.
775 drop(input_tx);
776 let result = supervisor.shutdown().await;
777 assert!(
778 result.is_ok(),
779 "child should have drained rather than been aborted: {result:?}"
780 );
781 assert_eq!(drained.load(Ordering::SeqCst), 1);
782 }
783
784 #[tokio::test]
785 async fn worker_without_a_terminal_condition_is_halted_at_once_when_marked_brutal() {
786 // The counterpart: work that never ends has to say so, otherwise it holds the drain open until its owner's
787 // timeout. `Brutal` is how it says so, and the shutdown is still clean.
788 let supervisor = TestSupervisor::start_with_worker_timeout("test_component", Duration::from_secs(30)).await;
789
790 supervisor.spawn_worker("component", |shutdown| async move {
791 crate::runtime::worker("endless", std::future::pending::<()>())
792 .with_shutdown_strategy(ShutdownStrategy::Brutal)
793 .spawn();
794 shutdown.await;
795 });
796 supervisor.wait_for_running_descendants(2).await;
797
798 let started = tokio::time::Instant::now();
799 let result = supervisor.shutdown().await;
800 let elapsed = started.elapsed();
801
802 assert!(
803 result.is_ok(),
804 "halting a brutal child is not an unclean shutdown: {result:?}"
805 );
806 assert!(
807 elapsed < Duration::from_secs(5),
808 "the child should have been halted at once rather than held to the component's 30s timeout; took {elapsed:?}"
809 );
810 }
811
812 #[tokio::test]
813 async fn endless_worker_is_halted_with_the_component_it_belongs_to() {
814 // The child keeps its default five-second timeout, but the component's shorter timeout halts it first.
815 let supervisor = TestSupervisor::start_with_worker_timeout("test_component", Duration::from_millis(200)).await;
816
817 supervisor.spawn_worker("component", |shutdown| async move {
818 crate::runtime::worker("stuck", std::future::pending::<()>()).spawn();
819 shutdown.await;
820 });
821 supervisor.wait_for_running_descendants(2).await;
822
823 let started = tokio::time::Instant::now();
824 let result = supervisor.shutdown().await;
825 let elapsed = started.elapsed();
826
827 assert!(
828 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
829 "the stuck child should have been halted with the component, and counted, got {result:?}"
830 );
831 assert!(
832 elapsed < Duration::from_secs(1),
833 "the child should have been halted with the component at 200ms rather than at its own timeout; took {elapsed:?}"
834 );
835 }
836
837 #[tokio::test]
838 async fn on_runtime_places_the_child_on_that_runtime() {
839 // `on_runtime` must actually change where the child's task runs, not just where it was spawned from.
840 let pool = tokio::runtime::Builder::new_multi_thread()
841 .worker_threads(1)
842 .thread_name("builder-pool-test")
843 .enable_all()
844 .build()
845 .expect("should build pool");
846
847 let supervisor = TestSupervisor::start("test_component").await;
848
849 let (thread_tx, thread_rx) = oneshot::channel();
850 supervisor
851 .handle()
852 .worker("pooled", async move {
853 let _ = thread_tx.send(std::thread::current().name().unwrap_or_default().to_string());
854 std::future::pending::<()>().await;
855 })
856 .on_runtime(pool.handle().clone())
857 .with_shutdown_strategy(ShutdownStrategy::Brutal)
858 .spawn();
859
860 let thread_name = timeout(Duration::from_secs(5), thread_rx)
861 .await
862 .expect("child should report its thread promptly")
863 .expect("child should not be dropped before reporting");
864 assert!(
865 thread_name.starts_with("builder-pool-test"),
866 "child must run on the given runtime, but ran on thread {thread_name:?}"
867 );
868
869 assert!(supervisor.shutdown().await.is_ok());
870 pool.shutdown_background();
871 }
872
873 #[tokio::test]
874 async fn child_exiting_does_not_shut_the_supervisor_down() {
875 // Spawned children are hosted. Thus, a child that finishes must not stop the supervisor, because that also
876 // stops the component. During a drain, a child that finishes is the normal case.
877 let supervisor = TestSupervisor::start("test_component").await;
878 let handle = supervisor.handle();
879
880 // Wait for the child to have actually run and exited. `wait_for_children(0)` would be vacuously true before
881 // the supervisor ever picked it up, so the exit this test is about would never be observed.
882 let exited = Arc::new(AtomicUsize::new(0));
883 let child_exited = Arc::clone(&exited);
884 handle.spawn_worker("brief", async move {
885 child_exited.fetch_add(1, Ordering::SeqCst);
886 });
887 wait_until("the child has run and exited", || exited.load(Ordering::SeqCst) == 1).await;
888 supervisor.wait_for_children(0).await;
889
890 // The supervisor survived that exit: it is still running and still accepting work.
891 assert!(handle.is_running());
892 handle
893 .worker("second", std::future::pending::<()>())
894 .with_shutdown_strategy(ShutdownStrategy::Brutal)
895 .spawn();
896
897 assert!(supervisor.shutdown().await.is_ok());
898 }
899
900 #[tokio::test]
901 async fn a_needed_child_exiting_stops_the_supervisor() {
902 // The counterpart: when a child that is marked needed terminates permanently, its supervisor also stops. Here
903 // the child exits cleanly. It is not obvious that a clean exit also stops the supervisor.
904 let supervisor = TestSupervisor::start("test_component").await;
905 let handle = supervisor.handle();
906
907 handle.worker("brief", async {}).needed().spawn();
908
909 // Spawning only queues the child, so wait for the supervisor to actually stop itself rather than racing our
910 // own shutdown against the child ever starting.
911 wait_until("the supervisor has stopped", || !handle.is_running()).await;
912
913 let result = supervisor.shutdown().await;
914 assert!(
915 matches!(result, Err(SupervisorError::SignificantChildExited)),
916 "a needed child's exit should have stopped the supervisor, got {result:?}"
917 );
918 }
919
920 #[tokio::test]
921 async fn spawned_children_record_poll_metrics() {
922 // Every supervised worker's task is timed, and a child spawned through the builder is no exception. The tag is
923 // the child's fully qualified process name, which is what gives one series per name rather than per task.
924 let recorder = TestRecorder::default();
925 let _guard = metrics::set_default_local_recorder(&recorder);
926
927 // The recorder must be installed before the child is spawned: metric handles are resolved once, at spawn.
928 let supervisor = TestSupervisor::start("metrics_component").await;
929
930 // Spawning only queues the child, so wait for it to actually run before shutting down -- otherwise there's no
931 // guarantee it was ever polled, and therefore none that it recorded anything.
932 let (ran_tx, ran_rx) = oneshot::channel();
933 supervisor.handle().spawn_worker("instrumented", async move {
934 let _ = ran_tx.send(());
935 });
936 ran_rx.await.expect("child should have run");
937
938 assert!(supervisor.shutdown().await.is_ok());
939
940 let polls = recorder.counter((
941 "runtime_task_poll_count",
942 &[("task_name", "metrics_component.instrumented")],
943 ));
944 assert!(
945 polls.is_some_and(|polls| polls > 0),
946 "spawned child should have recorded poll metrics, got {polls:?}"
947 );
948 }
949
950 #[tokio::test]
951 async fn supervisable_child_can_be_configured_before_spawning() {
952 // A `Supervisable` child goes through the same builder, including runtime placement.
953 let worker_pool_thread_id = Arc::new(Mutex::new(None));
954 let worker_pool_thread_id2 = Arc::clone(&worker_pool_thread_id);
955
956 let pool = tokio::runtime::Builder::new_multi_thread()
957 .worker_threads(1)
958 .on_thread_start(move || {
959 worker_pool_thread_id2
960 .lock()
961 .unwrap()
962 .replace(std::thread::current().id());
963 })
964 .enable_all()
965 .build()
966 .expect("should build pool");
967
968 let supervisor = TestSupervisor::start("test_component").await;
969
970 let (worker, initializations, worker_thread_id) = CountingWorker::new("counting");
971 supervisor
972 .handle()
973 .supervisable(worker)
974 .on_runtime(pool.handle().clone())
975 .spawn();
976
977 // `spawn` returns once the child is queued, and `initialize` runs inside the child's own task -- on the pool's
978 // runtime here -- so wait for it rather than assuming it has been polled.
979 supervisor.wait_for_children(1).await;
980 wait_until("the worker has initialized once", || {
981 initializations.load(Ordering::SeqCst) == 1
982 })
983 .await;
984
985 // Assert where it actually ran, not just that it ran: without this, `on_runtime` could be a no-op and the test
986 // would still pass.
987 let worker_pool_thread_id = worker_pool_thread_id
988 .lock()
989 .unwrap()
990 .expect("worker pool thread should have recorded its thread ID");
991 let worker_thread_id = worker_thread_id
992 .lock()
993 .unwrap()
994 .expect("worker should have recorded its thread ID");
995 assert_eq!(
996 worker_pool_thread_id, worker_thread_id,
997 "child should have run on the given runtime"
998 );
999
1000 assert!(supervisor.shutdown().await.is_ok());
1001 pool.shutdown_background();
1002 }
1003
1004 #[tokio::test]
1005 async fn supervisable_children_are_restarted_by_default() {
1006 let supervisor = TestSupervisor::start("test_component").await;
1007
1008 let (worker, initializations) = FailingOnceWorker::new();
1009 supervisor.handle().spawn_supervisable(worker);
1010
1011 // The worker fails its first run, but then runs forever after that, so we should observe two initializations
1012 // and no more after that.
1013 wait_until("the worker has initialized twice", || {
1014 initializations.load(Ordering::SeqCst) == 2
1015 })
1016 .await;
1017 assert!(supervisor.shutdown().await.is_ok());
1018 }
1019
1020 #[tokio::test]
1021 #[allow(deprecated)]
1022 async fn a_worker_that_cannot_restart_overrides_an_explicit_restart_policy() {
1023 // A worker that reports that it cannot start again is never restarted by its owner. This is also true if the
1024 // deprecated `transient` gave it an explicit policy that restarts it after a failure.
1025 let supervisor = TestSupervisor::start("test_component").await;
1026
1027 let (worker, initializations) = FailingOnceWorker::new();
1028 supervisor.handle().supervisable(worker.one_shot()).transient().spawn();
1029
1030 // The child starts and fails. If the supervisor restarted it, the child would stay registered, because each
1031 // later run waits for shutdown.
1032 wait_until("the worker has initialized once", || {
1033 initializations.load(Ordering::SeqCst) == 1
1034 })
1035 .await;
1036 supervisor.wait_for_children(0).await;
1037 assert_eq!(initializations.load(Ordering::SeqCst), 1);
1038
1039 assert!(supervisor.shutdown().await.is_ok());
1040 }
1041
1042 #[tokio::test]
1043 #[allow(deprecated)]
1044 async fn a_significant_child_that_is_always_restarted_is_accepted() {
1045 // The deprecated `with_significant` is available on the builder of a child that can start again. Declared in
1046 // advance, such a child is needed and permanent, so the flag has no effect. The supervisor warns about that,
1047 // but must accept the child and run it.
1048 let (worker, initializations, _) = CountingWorker::new("counting");
1049 let mut sup = Supervisor::new("test_sup").expect("supervisor name should be valid");
1050 sup.add_worker(supervisable(worker).with_significant(true).build());
1051
1052 let (tx, rx) = oneshot::channel::<()>();
1053 let run = tokio::spawn(async move {
1054 sup.run_with_shutdown(async move {
1055 let _ = rx.await;
1056 })
1057 .await
1058 });
1059 wait_until("the worker has initialized once", || {
1060 initializations.load(Ordering::SeqCst) == 1
1061 })
1062 .await;
1063
1064 tx.send(()).expect("supervisor should still be running");
1065 let result = run.await.expect("supervisor task should not panic");
1066 assert!(result.is_ok(), "supervisor should stop cleanly: {result:?}");
1067 }
1068
1069 #[tokio::test]
1070 async fn spawning_after_shutdown_never_starts_the_child() {
1071 let supervisor = TestSupervisor::start("test_component").await;
1072 let handle = supervisor.handle();
1073 assert!(supervisor.shutdown().await.is_ok());
1074
1075 let started = Arc::new(AtomicUsize::new(0));
1076 let child_started = Arc::clone(&started);
1077 handle.spawn_worker("late", async move {
1078 child_started.fetch_add(1, Ordering::SeqCst);
1079 });
1080
1081 tokio::time::sleep(Duration::from_millis(50)).await;
1082 assert_eq!(
1083 started.load(Ordering::SeqCst),
1084 0,
1085 "a child spawned against a stopped supervisor must never run"
1086 );
1087 }
1088}