Skip to main content

Module scope

Module scope 

Source
Expand description

Structured concurrency scopes.

A supervisor runs a set of children and restarts them when they fail. A scope is the same mechanism, but a single process owns it instead of a supervisor. The children that a worker spawns into its scope belong to that worker. Each child runs as a separate task, but the worker does not finish until all of its children finish.

When the body of the worker finishes, the scope closes and tells its children to stop. The body finishes when it returns, panics, or is cut short because a child that it needs failed. If the worker is halted, the scope halts its children instead. A request for the worker to stop does not close the scope by itself.

Every supervised worker has a scope. The runtime creates the scope at the first spawn into it, so a worker that never spawns a child uses no resources for a scope. The supervision tree shows the children of a worker below that worker.

§How a scope closes

A scope closes when the body of its owner finishes. It then stops each child according to the child’s own shutdown strategy, the same as a supervisor does: a graceful child is told to stop and halted if it outlives its timeout, and a brutal child is halted at once. A closure-based child defaults to graceful, with a timeout of five seconds.

The body can spawn children right before it returns, before they start. If the body returns normally, these children still start, and then they are told to stop. If the body fails, they never start.

The scope does not close only because the owner was told to stop. The owner decides when its children stop, because its scope closes only when its body returns. Thus, the owner can stop its children in the order that it chooses. For example, it can refuse new connections first, and then close the connections that it has. The signal that the children get when the scope closes is the last step of that order. A child that already stopped, for example because its input closed, has nothing left to do then. The one exception is a nested supervisor that stops with its owner, which is told to stop together with the owner.

The timeout of a child starts only when the child is told to stop, which is when the scope closes, after the body of the owner returns. Thus, the shutdown strategy of the owner must cover the time for its own body to stop and the time for its children to stop. If the owner’s timeout expires, the owner and everything still running in its scope, nested scopes included, are halted, logged, and counted, whether or not they were already told to stop. A child timeout can’t extend past its owner’s.

§Nested supervisors that stop with their owner

Some owners consume what their children produce. For example, the body of a relay drains a channel that an HTTP server in its scope feeds, and dispatches each request downstream. The server is a nested supervisor. The body can finish its drain only once the server stops, but the server would stop only once the body returns.

NestedSupervisorBuilder::stops_with_owner resolves this. When the owner is told to stop, the scope tells the server to stop at once, while the body still runs. The server stops accepting connections and completes the requests that it accepted. When it exits, the scope drops its specification, and with it the handler that holds the last sender of the channel. The body sees the channel close, and returns once it dispatched what remains.

  • The setting takes effect only if the owner takes the shutdown signal (see Supervisable::wants_shutdown_signal). Otherwise, the nested supervisor stops when the scope closes.
  • A nested supervisor with the setting that is spawned after its owner was told to stop is told to stop at once.
  • Its exit is a requested exit, because its owner was told to stop. Thus, the scope never restarts it, and it never fails the owner, even if it is needed.

The body of the owner MUST NOT hold a sender of a channel that it drains. Otherwise, the channel never closes, and the owner is halted when its timeout expires.

Other kinds of ingress need no setting:

  • A closure child can stop on the owner’s own shutdown handle, which the body moves into it.
  • The consumer can be a child too, which takes no signal and has no timeout of its own. The body then returns on the signal. As the scope closes, the ingress stops, and the consumer drains its channel until it closes.

§Requested exits and other exits

If a child exits after its owner, or a process above its owner, was told to stop, the exit is a requested exit. This is true even if the scope is still open. The scope never restarts a child after a requested exit, and a requested exit never fails the owner. Thus, a child that exits while its owner drains cannot cut that drain short.

The scope never restarts a child while the scope closes. If the child exits cleanly, the exit is also a requested exit, whatever the reason that the scope closes. But if the child fails and nothing told its owner to stop, the failure has the same effect as when the scope is open: a needed child fails the owner. This is necessary because a body often returns because a child failed. For example, the body sees the output channel of the child close. The body then returns before the scope sees the exit of the child, so the scope sees the failure only as it closes. If the body itself failed, its own error has priority.

If a child exits while its scope is open and no stop was requested, the child stopped by itself. Two things then decide what occurs next:

  • Its restart policy decides if the scope restarts it, the same as under a supervisor.
  • Its edge decides what occurs if the scope does not restart it. The default is a hosted child: the scope reaps it, and its owner continues. A needed child is a child that its owner cannot work without, and it fails the owner. To mark a child as needed, use ChildBuilder::needed.

A child can stop again and again. If one more restart exceeds the restart limit of the scope, the child fails the owner. The edge of the child has no effect on this, and under a supervisor, the same child stops the supervisor. A restart is a reset. If a child continues to fail, the failure goes to the next level of the supervision tree, and that level tries again.

This also applies to the children of a nested scope: they fail the process that the scope belongs to.

§How to spawn children

spawn and ChildBuilder::spawn spawn into the scope of the current process. current returns a handle to that scope, and you can move that handle to other tasks. Some code can run fully outside supervision, for example a primitive that a test constructs. That code uses spawn_or_detached or nested_or_detached. If there is no scope, these functions run the work as a detached task, the same as without scopes.

§Scopes owned by values

nested creates a scope that a value owns instead of a process. Its children run under the current process, but the ScopeGuard that it returns stops them when the guard is dropped. For example, a cache can hold the guard for its background tasks. The tasks then stop when the last copy of the cache is dropped or when the process that owns the cache stops, whichever occurs first. In both cases, nothing restarts the tasks, because they were told to stop. If the process stops before the nested scope can start, the nested scope closes at once, and its tasks never run.

A value can also own the scopes of the values that it builds. To do this, it builds them inside ScopeGuard::enter_sync. For example, a resolver that builds its own cache holds a guard, and builds the cache inside that guard. The scope of the cache is then nested in the scope of the resolver, and the tasks of the cache stop when the resolver stops. Only build a value inside the scope of another value if it never outlives that value.

§Pre-created scopes

UnadoptedScope::new creates a scope before the process that later owns it exists. The scope holds the children that are spawned into it until a worker adopts it. Then the children start as children of that worker. This is how the process that runs a component can own work that was spawned while the component was built. An example is the background tasks of a cache that is constructed together with the component.

The UnadoptedScope holds the children that wait. Handles to the scope do not. Thus, if the UnadoptedScope is dropped before a worker adopts it, the children that wait are dropped too, even if one of them holds a handle to the scope.

Only one worker can adopt a scope. If that worker starts again, for example because its supervisor restarts, the new instance gets a new scope of its own. The pre-created scope closed when the first instance stopped.

Structs§

Scope
A set of children owned by one process.
ScopeGuard
A scope that a value owns, which closes when the guard is dropped.
UnadoptedScope
A scope that no process owns yet.

Enums§

ScopeError
Errors that make a worker fail because of a child in its scope.

Functions§

current
Returns the scope of the current process.
nested
Creates a scope that a value owns instead of a process.
nested_or_detached
Creates a scope that a value owns, as nested does, or a detached guard that acts as a scope outside supervision.