Host-owned resource scopes shared by runs, timers, channels and compilation.
Select a scope with spec.policy.scope, or wrap compilation in with_scope/2.
The default scope belongs to the host caller. configure/2 makes the calling
host its lifetime owner; its exit or close/1 reclaims the scope's resources.
Policy keys are max_concurrent_runs, max_concurrent_compiles, max_queue,
wait_ms, priority (:background or :latency), heap_words (aggregate),
compile_heap_words (one compile worker's heap, OTP compilation included),
rate and compile_rate (%{quota: reductions, period_ms: ms}), churn
(%{heap_words: growth_words, gcs: minor_collections, period_ms: ms}),
sample_ms, max_violations, max_channels, max_buffer_slots, and
max_code_pins (distinct code slots the scope's live actors may pin).
Same-scope nested calls borrow an ancestor's permit of the same kind; their
workers remain members and share heap, reduction, churn and wall accounting.
A nested compile still needs a compile permit when its ancestor only holds a
run permit. Cross-scope nesting takes an independent permit. Nested requests
that cannot acquire immediately return busy/admission_cycle, avoiding
circular waits across per-scope run/compile admission.
Per-scope admission counts live runs, including blocked runs. This is an owner-selected lifetime policy, independent of node-wide runnable capacity. Node-wide runtime permits bound guest execution. Channel send/recv/alt, timer sleep, binding/syscall doors, nested calls and child/reply/monitor waits release capacity before waiting and reacquire before guest execution resumes. Native receives retain their existing permit for a probe bounded to one millisecond (zero for after-zero), then release for any remaining wait. Workers stay attached to all resource accounting while blocked.
Uncontended handoffs reserve an atomic ETS slot without a server call. The runtime keeps one monitored lifetime record across waits; slot insertion also records ownership, so death before the local commit cannot leak capacity. A spawned run worker opens that lifetime with its membership and retires it, with the run's live lease, when it detaches. An in-caller run is admitted, attached and given a lifetime by one call. Detaching is a request the run does not wait for; the run's later requests are ordered behind it. An uncontended spawned run makes one admission call in its caller and one Scope call in its worker; an uncontended in-caller run makes one call. A queue gate prevents these reservations from overtaking queued work. Closed lifetime records remain until the caller unregisters or dies, covering an interrupted insertion racing closure. The runtime configuration is read at startup and when the runtime becomes idle; changes do not resize live grants.
Runnable admission and reacquisition are FIFO within each priority class,
with one node-wide slot reserved for latency scopes. Their wait is unbounded
by wait_ms or max_queue; the run's wall deadline still applies. Closing or
terminating the scope returns a typed busy fault. Resumption is pinned to the
original scope lifetime, so closing and reusing a name cannot revive old work.
Suspended workers surrender capacity and reacquire before being resumed;
blocking cannot reset a scope's rate/churn counters or bypass suspension.
:runtime_concurrency configures total capacity (minimum two); the default is
max(256, 32 * schedulers_online). Latency-class scope calls use OTP priority
messages in Scope and Limits to avoid queued background bookkeeping. Their
callers wait for the reply at high process priority and drop back on
wake-up; a latency-class worker also starts at high and takes normal
before any other work. Scope and Limits wake at high only while idle and
handle every message at normal, so a backlog or a long handler competes
at normal and runs at most one time slice at high. Guest code never runs
above its class. A
monitored control sampler per scope inspects processes outside the shared
admission server, so a busy broker or worker cannot stall latency requests.
Stale samples cannot replace newer observations or cross member lifetimes.
Channel and buffer quotas are serialized in a controller for each scope,
including start-channel reservations and bulk alias admission. Each controller
owns its scope's counters row in a shared table and replaces only a row it
still owns, so a closed or reused name cannot be revived by a late
publication. A quiet controller hibernates. Allocation and free never
enter the shared Scope mailbox. Channel-only sampling stays local; configured
rate/churn policies and heap violations report to Scope for enforcement.
Worker sampling still combines worker and channel heaps in one scope budget.
Cumulative CPU/churn totals survive free, so short-lived brokers retain their
charges. Registry bills measured hot-path reductions to the scope controller.
Throttled allocation waits outside Registry and holds no runnable permit.
Channel brokers, control samplers and host allocation/free bookkeeping use
their allocating scope's priority class. Broker startup is partitioned by
scope and priority; retirement never blocks the shared supervisor. A protected
host that cannot be preempted retains its runnable permit after
closure/termination until it returns, blocks or exits.
Compile heaps are bounded separately by compile_heap_words and do not
consume the run aggregate heap or churn budget. Compile reductions still
consume compile_rate, and node-wide compile admission remains mandatory.
Scope enforcement never kills or suspends the owner or an in-caller host.
Those calls report busy/scope_terminated or busy/scope_closed on return;
arbitrary trusted host callbacks must cooperate to return control. Explicit
in-caller wall/reduction expiry likewise returns a typed fault on return,
rather than terminating the trusted host process. Engine-owned OTP actor
callbacks remain preemptible even though they run in their actor's process.
Offending scopes are suspended until refill and terminated after repeated
violations. Measurement is sampled; OS/node isolation is still necessary
for hostile tenants and native work that does not yield to the scheduler.
Summary
Functions
Returns a specification to start this module under a supervisor.
Close a scope, killing its workers and cancelling timers and channels.
Configure a scope owned by the calling host. Invalid policies fail closed.
Return the current host scope, defaulting to the calling host's lifetime.
Return host-only admission and throttle counters, without guest or binding data.
Run host work under a scope, restoring any enclosing scope afterwards.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
Close a scope, killing its workers and cancelling timers and channels.
Configure a scope owned by the calling host. Invalid policies fail closed.
Return the current host scope, defaulting to the calling host's lifetime.
Return host-only admission and throttle counters, without guest or binding data.
Run host work under a scope, restoring any enclosing scope afterwards.