saluki_core/runtime/scope/mod.rs
1//! Structured concurrency scopes.
2//!
3//! A supervisor runs a set of children and restarts them when they fail. A _scope_ is the same mechanism, but a single
4//! process owns it instead of a supervisor. The children that a worker spawns into its scope belong to that worker.
5//! Each child runs as a separate task, but the worker does not finish until all of its children finish.
6//!
7//! When the body of the worker finishes, the scope closes and tells its children to stop. The body finishes when it
8//! returns, panics, or is cut short because a child that it needs failed. If the worker is halted, the scope halts its
9//! children instead. A request for the worker to stop does not close the scope by itself.
10//!
11//! Every supervised worker has a scope. The runtime creates the scope at the first spawn into it, so a worker that
12//! never spawns a child uses no resources for a scope. The supervision tree shows the children of a worker below that
13//! worker.
14//!
15//! # How a scope closes
16//!
17//! A scope closes when the body of its owner finishes. It then stops each child according to the child's own shutdown
18//! strategy, the same as a supervisor does: a graceful child is told to stop and halted if it outlives its timeout, and
19//! a brutal child is halted at once. A closure-based child defaults to graceful, with a timeout of five seconds.
20//!
21//! The body can spawn children right before it returns, before they start. If the body returns normally, these
22//! children still start, and then they are told to stop. If the body fails, they never start.
23//!
24//! The scope does not close only because the owner was told to stop. The owner decides when its children stop, because
25//! its scope closes only when its body returns. Thus, the owner can stop its children in the order that it chooses. For
26//! example, it can refuse new connections first, and then close the connections that it has. The signal that the
27//! children get when the scope closes is the last step of that order. A child that already stopped, for example
28//! because its input closed, has nothing left to do then. The one exception is a nested supervisor that
29//! [stops with its owner](#nested-supervisors-that-stop-with-their-owner), which is told to stop together with the
30//! owner.
31//!
32//! The timeout of a child starts only when the child is told to stop, which is when the scope closes, after the body
33//! of the owner returns. Thus, the shutdown strategy of the owner must cover the time for its own body to stop and the
34//! time for its children to stop. If the owner's timeout expires, the owner and everything still running in its scope,
35//! nested scopes included, are halted, logged, and counted, whether or not they were already told to stop. A child
36//! timeout can't extend past its owner's.
37//!
38//! # Nested supervisors that stop with their owner
39//!
40//! Some owners consume what their children produce. For example, the body of a relay drains a channel that an HTTP
41//! server in its scope feeds, and dispatches each request downstream. The server is a nested supervisor. The body can
42//! finish its drain only once the server stops, but the server would stop only once the body returns.
43//!
44//! [`NestedSupervisorBuilder::stops_with_owner`][stops] resolves this. When the owner is told to stop, the scope tells
45//! the server to stop at once, while the body still runs. The server stops accepting connections and completes the
46//! requests that it accepted. When it exits, the scope drops its specification, and with it the handler that holds the
47//! last sender of the channel. The body sees the channel close, and returns once it dispatched what remains.
48//!
49//! - The setting takes effect only if the owner takes the shutdown signal (see
50//! [`Supervisable::wants_shutdown_signal`]). Otherwise, the nested supervisor stops when the scope closes.
51//! - A nested supervisor with the setting that is spawned after its owner was told to stop is told to stop at once.
52//! - Its exit is a requested exit, because its owner was told to stop. Thus, the scope never restarts it, and it never
53//! fails the owner, even if it is needed.
54//!
55//! The body of the owner **MUST NOT** hold a sender of a channel that it drains. Otherwise, the channel never closes,
56//! and the owner is halted when its timeout expires.
57//!
58//! Other kinds of ingress need no setting:
59//!
60//! - A closure child can stop on the owner's own shutdown handle, which the body moves into it.
61//! - The consumer can be a child too, which takes no signal and has no timeout of its own. The body then returns on
62//! the signal. As the scope closes, the ingress stops, and the consumer drains its channel until it closes.
63//!
64//! [stops]: crate::runtime::NestedSupervisorBuilder::stops_with_owner
65//!
66//! # Requested exits and other exits
67//!
68//! If a child exits after its owner, or a process above its owner, was told to stop, the exit is a requested exit. This
69//! is true even if the scope is still open. The scope never restarts a child after a requested exit, and a requested
70//! exit never fails the owner. Thus, a child that exits while its owner drains cannot cut that drain short.
71//!
72//! The scope never restarts a child while the scope closes. If the child exits cleanly, the exit is also a requested
73//! exit, whatever the reason that the scope closes. But if the child fails and nothing told its owner to stop, the
74//! failure has the same effect as when the scope is open: a needed child fails the owner. This is necessary because a
75//! body often returns because a child failed. For example, the body sees the output channel of the child close. The
76//! body then returns before the scope sees the exit of the child, so the scope sees the failure only as it closes. If
77//! the body itself failed, its own error has priority.
78//!
79//! If a child exits while its scope is open and no stop was requested, the child stopped by itself. Two things then
80//! decide what occurs next:
81//!
82//! - Its restart policy decides if the scope restarts it, the same as under a supervisor.
83//! - Its _edge_ decides what occurs if the scope does not restart it. The default is a _hosted_ child: the scope reaps
84//! it, and its owner continues. A _needed_ child is a child that its owner cannot work without, and it fails the
85//! owner. To mark a child as needed, use [`ChildBuilder::needed`][crate::runtime::ChildBuilder::needed].
86//!
87//! A child can stop again and again. If one more restart exceeds the restart limit of the scope, the child fails the
88//! owner. The edge of the child has no effect on this, and under a supervisor, the same child stops the supervisor. A
89//! restart is a reset. If a child continues to fail, the failure goes to the next level of the supervision tree, and
90//! that level tries again.
91//!
92//! This also applies to the children of a [nested] scope: they fail the process that the scope belongs to.
93//!
94//! # How to spawn children
95//!
96//! [`spawn`][crate::runtime::spawn] and [`ChildBuilder::spawn`][crate::runtime::ChildBuilder::spawn] spawn into the
97//! scope of the current process. [`current`] returns a handle to that scope, and you can move that handle to other
98//! tasks. Some code can run fully outside supervision, for example a primitive that a test constructs. That code uses
99//! [`spawn_or_detached`][crate::runtime::ChildBuilder::spawn_or_detached] or [`nested_or_detached`]. If there is no
100//! scope, these functions run the work as a detached task, the same as without scopes.
101//!
102//! # Scopes owned by values
103//!
104//! [`nested`] creates a scope that a value owns instead of a process. Its children run under the current process, but
105//! the [`ScopeGuard`] that it returns stops them when the guard is dropped. For example, a cache can hold the guard for
106//! its background tasks. The tasks then stop when the last copy of the cache is dropped or when the process that owns
107//! the cache stops, whichever occurs first. In both cases, nothing restarts the tasks, because they were told to stop.
108//! If the process stops before the nested scope can start, the nested scope closes at once, and its tasks never run.
109//!
110//! A value can also own the scopes of the values that it builds. To do this, it builds them inside
111//! [`ScopeGuard::enter_sync`]. For example, a resolver that builds its own cache holds a guard, and builds the cache
112//! inside that guard. The scope of the cache is then nested in the scope of the resolver, and the tasks of the cache
113//! stop when the resolver stops. Only build a value inside the scope of another value if it never outlives that value.
114//!
115//! # Pre-created scopes
116//!
117//! [`UnadoptedScope::new`] creates a scope before the process that later owns it exists. The scope holds the children
118//! that are spawned into it until a worker adopts it. Then the children start as children of that worker. This is how
119//! the process that runs a component can own work that was spawned while the component was built. An example is the
120//! background tasks of a cache that is constructed together with the component.
121//!
122//! The [`UnadoptedScope`] holds the children that wait. Handles to the scope do not. Thus, if the `UnadoptedScope` is
123//! dropped before a worker adopts it, the children that wait are dropped too, even if one of them holds a handle to the
124//! scope.
125//!
126//! Only one worker can adopt a scope. If that worker starts again, for example because its supervisor restarts, the
127//! new instance gets a new scope of its own. The pre-created scope closed when the first instance stopped.
128
129mod drive;
130use std::{
131 fmt,
132 future::Future,
133 sync::{
134 atomic::{AtomicU64, Ordering},
135 Arc, Mutex, OnceLock,
136 },
137 time::Duration,
138};
139
140use async_trait::async_trait;
141use saluki_common::{
142 sync::shutdown::{ShutdownCoordinator, ShutdownHandle},
143 task::spawn_traced_named,
144};
145use snafu::Snafu;
146use tokio::{runtime::Handle, select, sync::mpsc};
147use tracing::{debug, warn};
148
149pub(super) use self::drive::{drive, DriveContext, StopLink};
150use super::{
151 supervisable::{InitializationError, ShutdownStrategy, Supervisable, SupervisorFuture},
152 supervisor::{ChildConfig, ChildId, ChildSpecification, ChildState, Registration, SupervisedChild, WorkerSpec},
153};
154
155/// How the termination of a child affects its owner, which is its supervisor or the process that owns its scope.
156///
157/// The owner uses the edge only if a child terminates without a request to stop and its restart policy does not restart
158/// it. If the restart policy requires a restart but the child reached the restart limit of its owner, the child fails
159/// the owner. The edge of the child has no effect on this.
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
161pub(crate) enum Edge {
162 /// The owner only runs the child: if the child terminates and is not restarted, it is reaped and the owner
163 /// continues.
164 #[default]
165 Hosted,
166
167 /// The owner cannot do its work without the child: if the child terminates and is not restarted, the owner fails.
168 Needed,
169}
170
171/// Errors that make a worker fail because of a child in its scope.
172#[derive(Debug, Snafu)]
173#[snafu(context(suffix(false)))]
174pub enum ScopeError {
175 /// A child terminated without a request to stop and was not restarted, and its owner cannot continue without it.
176 #[snafu(display("Child '{}' terminated: {}", child_name, reason))]
177 ChildTerminated {
178 /// The name of the child.
179 child_name: String,
180
181 /// How the child terminated.
182 reason: String,
183 },
184
185 /// A child terminated repeatedly without a request to stop and exceeded the restart limit of its scope.
186 #[snafu(display("Child '{}' exceeded its scope's restart limit.", child_name))]
187 ChildRestartLimit {
188 /// The name of the child.
189 child_name: String,
190 },
191}
192
193/// A child that is queued for a scope and waits for the owner of the scope to start it.
194struct PendingScopeChild {
195 id: u64,
196 spec: SupervisedChild,
197 config: ChildConfig,
198}
199
200/// The queue of children that wait for the owner of a scope to start them.
201type SpawnQueue = mpsc::UnboundedReceiver<PendingScopeChild>;
202
203/// A set of children owned by one process.
204///
205/// See the [module documentation][self] for a description of a scope. A `Scope` is a handle. A clone of it costs
206/// little, and any task can use it. A spawn through a `Scope` always succeeds. But, as with [`tokio::spawn`], a
207/// successful spawn does not guarantee that the child runs. If a child is spawned after its scope closed, the child is
208/// dropped.
209///
210/// A handle does not hold the children that wait to start. The owner of the scope holds them, or the
211/// [`UnadoptedScope`] before a worker adopts the scope. Thus, a child can hold a handle to its own scope without
212/// keeping the scope alive.
213#[derive(Clone)]
214pub struct Scope {
215 shared: Arc<ScopeShared>,
216}
217
218struct ScopeShared {
219 name: Arc<str>,
220 tx: mpsc::UnboundedSender<PendingScopeChild>,
221 next_id: AtomicU64,
222}
223
224impl Scope {
225 /// Creates a scope, and returns the handle to it together with its spawn queue.
226 fn with_queue(name: String) -> (Self, SpawnQueue) {
227 let (tx, rx) = mpsc::unbounded_channel();
228 let scope = Self {
229 shared: Arc::new(ScopeShared {
230 name: Arc::from(name),
231 tx,
232 next_id: AtomicU64::new(0),
233 }),
234 };
235 (scope, rx)
236 }
237
238 /// Returns the scope's name.
239 pub fn name(&self) -> &str {
240 &self.shared.name
241 }
242
243 /// Spawns a child into the scope.
244 ///
245 /// Accepts all values that [`SupervisorHandle::spawn`][crate::runtime::SupervisorHandle::spawn] accepts. This
246 /// includes a nested supervisor. By default, the child is hosted.
247 ///
248 /// The returned [`ChildId`] identifies the child in this scope.
249 pub fn spawn<S, T>(&self, child: T) -> ChildId
250 where
251 S: ChildState,
252 T: Into<ChildSpecification<S>>,
253 {
254 let (spec, config) = S::into_child_parts(child.into(), Registration::Spawned).into_parts();
255 self.enqueue(spec, config)
256 }
257
258 fn enqueue(&self, spec: SupervisedChild, config: ChildConfig) -> ChildId {
259 let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
260 if let Err(e) = self.shared.tx.send(PendingScopeChild { id, spec, config }) {
261 // A race with the shutdown of the owner is normal and not an exception, so this message stays at debug
262 // level.
263 debug!(
264 scope = %self.shared.name,
265 child_name = e.0.spec.name(),
266 "Scope has closed; child will not be started."
267 );
268 }
269
270 ChildId::from_raw(id)
271 }
272
273 /// Runs `fut` with this scope as the current one.
274 ///
275 /// If `fut` spawns into the current scope, the spawn goes into this scope instead. This applies to spawns through
276 /// [`current`], [`spawn`][crate::runtime::spawn], or [`nested`]. While `fut` runs, this scope replaces the scope
277 /// that was current before.
278 pub fn enter<F: Future>(&self, fut: F) -> impl Future<Output = F::Output> {
279 CURRENT_SCOPE.scope(ScopeSlot::prefilled(self.clone()), fut)
280 }
281
282 /// Runs `f` with this scope as the current one.
283 ///
284 /// This is the synchronous version of [`enter`][Self::enter].
285 pub fn enter_sync<R>(&self, f: impl FnOnce() -> R) -> R {
286 CURRENT_SCOPE.sync_scope(ScopeSlot::prefilled(self.clone()), f)
287 }
288}
289
290impl fmt::Debug for Scope {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 f.debug_struct("Scope")
293 .field("name", &self.shared.name)
294 .finish_non_exhaustive()
295 }
296}
297
298/// A scope that no process owns yet.
299///
300/// The scope holds the children that are spawned into it until a worker adopts it. Then the children start as
301/// children of that worker. If the `UnadoptedScope` is dropped before a worker adopts it, the scope closes, and it
302/// drops the children that wait and each child that is spawned into it later.
303///
304/// Most code does not use this type. Every supervised worker already has a scope, which is available through
305/// [`current`] or [`spawn`][crate::runtime::spawn].
306pub struct UnadoptedScope {
307 scope: Scope,
308
309 /// The children that wait for a worker to adopt the scope. Always present until the scope is adopted.
310 rx: Option<SpawnQueue>,
311}
312
313impl UnadoptedScope {
314 /// Creates a scope that no process owns yet.
315 pub fn new<N: Into<String>>(name: N) -> Self {
316 let (scope, rx) = Scope::with_queue(name.into());
317 Self { scope, rx: Some(rx) }
318 }
319
320 /// Returns a handle to the scope, through which children can be spawned into it before a worker adopts it.
321 pub fn scope(&self) -> &Scope {
322 &self.scope
323 }
324
325 /// Describes a worker that owns this scope, for a supervisor to add as a child.
326 ///
327 /// The worker only holds the children of the scope. It runs until it is told to stop, and then the scope closes.
328 /// Use this for work that is spawned outside supervised processes, such as work at startup before the supervision
329 /// tree runs. The worker makes that work part of the tree when the tree runs. The worker fails if one of the
330 /// children of the scope fails its owner. The worker passes that failure to the supervisor or scope that it is
331 /// added to.
332 ///
333 /// If the supervisor starts the worker again, for example because the supervisor restarts, the new worker holds
334 /// nothing. The scope closed when the first worker stopped, and the scope drops each child that is spawned into it
335 /// after that.
336 pub fn into_host(self) -> ChildSpecification<WorkerSpec> {
337 let name = self.scope.name().to_string();
338 ChildSpecification::worker(ScopeHost::new(name, None))
339 .forwarding_failures()
340 .adopting(self)
341 }
342
343 /// Splits the scope into its handle and its spawn queue, for the worker that adopts it.
344 fn into_parts(mut self) -> (Scope, SpawnQueue) {
345 let rx = self.rx.take().expect("an unadopted scope holds its spawn queue");
346 (self.scope.clone(), rx)
347 }
348}
349
350impl Drop for UnadoptedScope {
351 fn drop(&mut self) {
352 // If no worker adopted a scope, the scope drops all children that are in its queue. This is normal. For
353 // example, a component that fails before it runs never adopts the scope that it was built in. Thus, this
354 // message stays at debug level.
355 let Some(mut rx) = self.rx.take() else {
356 return;
357 };
358
359 rx.close();
360 let mut dropped = 0;
361 while rx.try_recv().is_ok() {
362 dropped += 1;
363 }
364 if dropped > 0 {
365 debug!(
366 scope = self.scope.name(),
367 dropped, "Scope was never adopted; dropping the children queued on it."
368 );
369 }
370 }
371}
372
373impl fmt::Debug for UnadoptedScope {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 f.debug_struct("UnadoptedScope")
376 .field("name", &self.scope.name())
377 .finish_non_exhaustive()
378 }
379}
380
381/// A pre-created scope for a worker to adopt, shared by every copy of the configuration of that worker.
382///
383/// The first instance of the worker takes the scope. A later instance finds nothing, and gets a new scope instead. If
384/// the last copy of the configuration is dropped before any instance takes the scope, the scope closes and drops the
385/// children that wait in it.
386#[derive(Clone)]
387pub(crate) struct Adoption {
388 name: Arc<str>,
389 scope: Arc<Mutex<Option<UnadoptedScope>>>,
390}
391
392impl Adoption {
393 pub(crate) fn new(scope: UnadoptedScope) -> Self {
394 Self {
395 name: Arc::clone(&scope.scope.shared.name),
396 scope: Arc::new(Mutex::new(Some(scope))),
397 }
398 }
399
400 /// Returns the name of the scope.
401 fn name(&self) -> &str {
402 &self.name
403 }
404
405 /// Takes the scope, if no instance of the worker took it yet.
406 fn take(&self) -> Option<UnadoptedScope> {
407 self.scope.lock().expect("adoption lock poisoned").take()
408 }
409}
410
411impl fmt::Debug for Adoption {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 f.debug_struct("Adoption")
414 .field("scope", &self.name)
415 .finish_non_exhaustive()
416 }
417}
418
419tokio::task_local! {
420 /// The scope of the current process.
421 static CURRENT_SCOPE: ScopeSlot;
422}
423
424/// Where the current process keeps its scope.
425///
426/// The runtime installs this slot around the body of each supervised worker. The runtime creates the scope of a worker
427/// only at the first spawn into it. Thus, a worker that never spawns a child never has a scope.
428#[derive(Clone)]
429struct ScopeSlot {
430 /// How the slot creates its scope on demand, or `None` if the slot received its scope at creation.
431 lazy: Option<Arc<LazyScope>>,
432
433 scope: Arc<OnceLock<Scope>>,
434}
435
436/// What a slot needs to create its scope on demand.
437struct LazyScope {
438 name: Arc<str>,
439
440 /// The spawn queue of the scope after the slot created it, until the worker that owns the slot takes it.
441 rx: Mutex<Option<SpawnQueue>>,
442}
443
444impl ScopeSlot {
445 /// Creates a slot that creates its scope, with the name `name`, at the first use.
446 fn lazy(name: Arc<str>) -> Self {
447 Self {
448 lazy: Some(Arc::new(LazyScope {
449 name,
450 rx: Mutex::new(None),
451 })),
452 scope: Arc::new(OnceLock::new()),
453 }
454 }
455
456 /// Creates a slot that holds `scope`.
457 fn prefilled(scope: Scope) -> Self {
458 Self {
459 lazy: None,
460 scope: Arc::new(OnceLock::from(scope)),
461 }
462 }
463
464 /// Returns the scope, if it exists.
465 fn get(&self) -> Option<&Scope> {
466 self.scope.get()
467 }
468
469 /// Returns the scope, and first creates it if this slot creates its scope on demand.
470 fn get_or_create(&self) -> Option<Scope> {
471 if let Some(scope) = self.scope.get() {
472 return Some(scope.clone());
473 }
474
475 let lazy = self.lazy.as_ref()?;
476 let scope = self.scope.get_or_init(|| {
477 let (scope, rx) = Scope::with_queue(lazy.name.to_string());
478 *lazy.rx.lock().expect("scope slot lock poisoned") = Some(rx);
479 scope
480 });
481 Some(scope.clone())
482 }
483
484 /// Takes the spawn queue of the scope that this slot created on demand, if the slot created one.
485 fn take_receiver(&self) -> Option<SpawnQueue> {
486 self.lazy.as_ref()?.rx.lock().expect("scope slot lock poisoned").take()
487 }
488}
489
490/// Returns the scope of the current process.
491///
492/// Returns `None` outside of a supervised process. You can move the returned handle to other tasks. Thus, code that
493/// does not run as the process that owns the scope can spawn into that scope. A callback-driven executor is an example
494/// of such code.
495pub fn current() -> Option<Scope> {
496 CURRENT_SCOPE.try_with(ScopeSlot::get_or_create).ok().flatten()
497}
498
499/// Creates a scope that a value owns instead of a process.
500///
501/// The children of the scope run under the current process. The supervision tree shows them below that process, under
502/// `name`. They stop when the returned guard is dropped or when the current process stops, whichever occurs first. In
503/// both cases, nothing restarts them. If the current process stops before the scope can start, the scope closes at
504/// once, and its children never run.
505///
506/// # Panics
507///
508/// Panics if there is no current scope. This means that the caller does not run in a supervised process. Use
509/// [`nested_or_detached`] for code that can run outside supervision.
510pub fn nested<N: Into<String>>(name: N) -> ScopeGuard {
511 let owner = current().unwrap_or_else(|| {
512 panic!(
513 "`scope::nested` called outside of a supervised process: there is no current scope to nest in. Use \
514 `scope::nested_or_detached` for code that may run outside supervision."
515 )
516 });
517
518 nested_under(&owner, name.into())
519}
520
521/// Creates a scope that a value owns, as [`nested`] does, or a detached guard that acts as a scope outside supervision.
522///
523/// Outside a supervised process, there is no scope to nest in. Thus, the returned guard runs its children as detached
524/// tasks instead. When the guard is dropped, it tells these tasks to stop. This is the behavior that this work has
525/// without scopes. For this reason, this function is the correct choice for primitives that code can construct
526/// anywhere.
527pub fn nested_or_detached<N: Into<String>>(name: N) -> ScopeGuard {
528 let name = name.into();
529 match current() {
530 Some(owner) => nested_under(&owner, name),
531 None => ScopeGuard {
532 inner: GuardInner::Detached {
533 name: Arc::from(name),
534 coordinator: ShutdownCoordinator::default(),
535 },
536 },
537 }
538}
539
540fn nested_under(owner: &Scope, name: String) -> ScopeGuard {
541 let unadopted = UnadoptedScope::new(name.clone());
542 let scope = unadopted.scope().clone();
543 let (close, close_signal) = ShutdownHandle::paired();
544
545 // The children of the nested scope must run under a process. Thus, a host worker adopts the scope and holds the
546 // children until the guard is dropped or the host is told to stop. The failures of the children belong to the
547 // owner, so the host passes its own failures to the owner.
548 //
549 // The specification of the host holds the scope until the host adopts it. If the host never starts, for example
550 // because the scope of the owner closed first, the specification is dropped. The nested scope then closes, and it
551 // drops the children that wait in it and each child that is spawned into it later.
552 let host = ChildSpecification::worker(ScopeHost::new(name, Some(close_signal)))
553 .forwarding_failures()
554 .adopting(unadopted);
555 owner.spawn(host);
556
557 ScopeGuard {
558 inner: GuardInner::Scoped { scope, _close: close },
559 }
560}
561
562/// A scope that a value owns, which closes when the guard is dropped.
563///
564/// [`nested`] and [`nested_or_detached`] return this guard. Keep the guard with the value that the scope's children
565/// serve, so that they stop when that value is dropped.
566#[must_use = "dropping the guard closes the scope, stopping everything spawned into it"]
567pub struct ScopeGuard {
568 inner: GuardInner,
569}
570
571enum GuardInner {
572 /// A nested scope, which closes when a drop of the coordinator fires the signal of its host worker.
573 Scoped { scope: Scope, _close: ShutdownCoordinator },
574
575 /// Detached tasks that act as a scope outside supervision, where each task holds a handle to this coordinator.
576 Detached {
577 name: Arc<str>,
578 coordinator: ShutdownCoordinator,
579 },
580}
581
582impl ScopeGuard {
583 /// Spawns a child into the scope.
584 ///
585 /// Returns the child's [`ChildId`], or `None` if the guard is detached and the child started as a detached task. A
586 /// detached child receives a shutdown signal that fires when the guard is dropped. It has no other supervision:
587 /// nothing restarts it, and nothing waits for it.
588 pub fn spawn<T>(&mut self, child: T) -> Option<ChildId>
589 where
590 T: Into<ChildSpecification<WorkerSpec>>,
591 {
592 match &mut self.inner {
593 GuardInner::Scoped { scope, .. } => Some(scope.spawn(child)),
594 GuardInner::Detached { coordinator, .. } => {
595 spawn_detached(child.into(), coordinator.register());
596 None
597 }
598 }
599 }
600
601 /// Runs `f` with the scope of this guard as the current scope.
602 ///
603 /// Each spawn into the current scope inside `f` goes into this scope instead. This includes each scope that `f`
604 /// creates with [`nested_or_detached`]. Thus, a value that builds other values can also own their background tasks.
605 /// For example, a resolver builds its caches inside its own scope. The tasks of the caches then appear below the
606 /// resolver, and they stop when the resolver is dropped.
607 ///
608 /// A detached guard has no scope, so it runs `f` with no change. The values that `f` builds then also run their
609 /// tasks detached.
610 pub fn enter_sync<R>(&self, f: impl FnOnce() -> R) -> R {
611 match &self.inner {
612 GuardInner::Scoped { scope, .. } => scope.enter_sync(f),
613 GuardInner::Detached { .. } => f(),
614 }
615 }
616
617 /// Returns `true` if the guard acts as a scope outside supervision and runs its children as detached tasks.
618 pub fn is_detached(&self) -> bool {
619 matches!(self.inner, GuardInner::Detached { .. })
620 }
621}
622
623impl fmt::Debug for ScopeGuard {
624 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625 match &self.inner {
626 GuardInner::Scoped { scope, .. } => f.debug_struct("ScopeGuard").field("scope", scope).finish(),
627 GuardInner::Detached { name, .. } => f
628 .debug_struct("ScopeGuard")
629 .field("detached", name)
630 .finish_non_exhaustive(),
631 }
632 }
633}
634
635/// Runs a worker as a detached task, outside supervision.
636///
637/// This function initializes the worker with `shutdown` and runs it to completion. It logs failures, because there is
638/// no other place to report them. The worker runs on the runtime that it was placed on, if it was placed on one. No
639/// other setting of the worker applies, because nothing supervises the worker.
640pub(super) fn spawn_detached(child: ChildSpecification<WorkerSpec>, shutdown: ShutdownHandle) {
641 let (spec, config) = WorkerSpec::into_child_parts(child, Registration::Spawned).into_parts();
642 let SupervisedChild::Worker(worker) = spec else {
643 unreachable!("a worker specification always lowers to a worker");
644 };
645
646 let name = worker.name().to_string();
647 let _runtime = config.runtime().map(Handle::enter);
648 spawn_traced_named(name.clone(), async move {
649 let run = match worker.initialize(shutdown).await {
650 Ok(run) => run,
651 Err(e) => {
652 warn!(child_name = %name, error = %e, "Detached child failed to initialize.");
653 return;
654 }
655 };
656
657 if let Err(e) = run.await {
658 warn!(child_name = %name, error = %e, "Detached child exited with an error.");
659 }
660 });
661}
662
663/// The worker that a nested or pre-created scope runs under.
664///
665/// Its body only waits until it is told to stop. Its supervisor can tell it to stop. For a nested scope, a drop of the
666/// guard also tells it to stop. When the body returns, the scope that the host adopted closes. That scope contains the
667/// actual work.
668struct ScopeHost {
669 name: String,
670 close: Mutex<Option<ShutdownHandle>>,
671}
672
673impl ScopeHost {
674 fn new(name: String, close: Option<ShutdownHandle>) -> Self {
675 Self {
676 name,
677 close: Mutex::new(close),
678 }
679 }
680}
681
682#[async_trait]
683impl Supervisable for ScopeHost {
684 fn name(&self) -> &str {
685 &self.name
686 }
687
688 fn shutdown_strategy(&self) -> ShutdownStrategy {
689 // The host has no timeout of its own. Its body returns as soon as it is told to stop, and then its scope stops
690 // each child according to the child's own shutdown strategy. The host waits for that, however long it takes.
691 ShutdownStrategy::Graceful(Duration::MAX)
692 }
693
694 fn can_restart(&self) -> bool {
695 // The host adopts its scope when it starts. That scope closes when the host stops, so a restarted host would
696 // hold nothing.
697 false
698 }
699
700 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
701 let close = self.close.lock().expect("scope host lock poisoned").take();
702
703 Ok(Box::pin(async move {
704 match close {
705 Some(close) => {
706 select! {
707 _ = process_shutdown => {},
708 _ = close => {},
709 }
710 }
711 None => process_shutdown.await,
712 }
713
714 Ok(())
715 }))
716 }
717}
718
719#[cfg(test)]
720mod tests;