Hyperlang.Scope (Hyperlang v0.1.0)

Copy Markdown View Source

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(scope)

Close a scope, killing its workers and cancelling timers and channels.

configure(scope, policy)

Configure a scope owned by the calling host. Invalid policies fail closed.

current()

Return the current host scope, defaulting to the calling host's lifetime.

stats(scope)

Return host-only admission and throttle counters, without guest or binding data.

with_scope(scope, fun)

Run host work under a scope, restoring any enclosing scope afterwards.