Skip to content

Latest commit

 

History

History
162 lines (137 loc) · 9.6 KB

PROCESS_MODEL.md

File metadata and controls

162 lines (137 loc) · 9.6 KB

Typed local processes

The native Process API adds isolated actor failures, independent monitors, links and typed exit signals. It shares the existing typed Pid(M) mailboxes, scheduler owners and copied messages. Existing spawn and supervise keep their established behavior. These are the first two stages of the local OTP plan. A pure supervisor engine now implements the pinned OTP strategies, restart window and management policies as a driver-owned state machine; Morrow programs still cannot start a typed Supervisor. Stage acceptance is recorded in the roadmap.

Creating and observing a process

Operation Result and context
Process.spawn(entry) Result(Pid(M), Process.Error); root or actor
Process.spawn_monitor(entry) Result((Pid(M), MonitorRef), Process.Error); actor only
Process.self() Current Pid(M); actor only
Process.id(pid) Opaque ProcessId; root or actor
Process.monitor(identity) Result(MonitorRef, Process.Error); actor only
Process.demonitor(reference, options) Result(Bool, Process.Error); owning actor only

An entry is a zero-argument Unit function with mailbox type M, as for ordinary actors. Atomic spawn-monitor installs the relationship before the child can execute. Expected admission errors are Results and must be handled. An isolated child's checked fault retires that child and sends its monitors a Down; an unrelated child can continue. Calling ordinary spawn inside an isolated actor still selects ordinary legacy failure behavior. Root faults, invalid native ABI use and invocation infrastructure failures remain invocation-wide failures.

ProcessId erases mailbox type for observation, without exposing an untyped send or a conversion back to a typed PID. MonitorRef identifies one independent registration: monitoring the same process twice gives two references and two notifications. Both opaque values support equality and copying through typed messages; copies retain the original invocation and generation identity.

See the executable native fixtures for complete examples: monitoring a failing worker and cancelling monitors and receiving events. These are library fixtures with a typed String reply port; their native harness supplies the host.

Receiving events

Ordinary receive examines user messages. receive_event examines the same ordered mailbox through Process.Event(M):

  • Process.Message(value) wraps an ordinary message of type M.
  • Process.Down(reference, identity, reason) reports one monitor completion.
  • Process.Exit(identity, reason) reports a trapped exit signal.

An ordinary receive skips lifecycle cells without consuming them. Sending a user value that happens to look like an event still produces Message(value); it cannot forge a system notification. A worker's earlier messages precede its Down, including when it crosses scheduler owners. Receive guards may compare opaque identities and references; process operations are forbidden in guards and defer bodies.

The reason schema is Normal, Shutdown, ShutdownDetail(String), Fault(Int), Failure(String), Kill, Killed and NoProcess, under the Process namespace. Normal return produces Normal, checked failure produces Fault, and observing an already dead local identity produces NoProcess. Explicit exits and signals can carry the other typed reasons. Fault payloads retain the full signed 64-bit value.

Links and exit signals

Operation Result and context
Process.link(identity) Result((), Process.Error); actor only
Process.unlink(identity) Result((), Process.Error); actor only
Process.spawn_link(entry) Result(Pid(M), Process.Error); actor only
Process.trap_exit(enabled) Previous Bool setting; actor only
Process.exit(reason) Terminates the current actor; actor only
Process.signal_exit(identity, reason) Result((), Process.Error); actor only

A link is symmetric and has one active relationship per pair. Repeated linking is idempotent; linking oneself is an inert success. Spawn-link installs both sides before the child can run and rolls back atomically if admission fails. Linking a dead local identity creates a NoProcess exit effect. Signalling an already dead valid local identity succeeds without reviving it.

Actors start with exit trapping disabled. An untrapped Normal signal is ignored; other untrapped reasons terminate the recipient. With trapping enabled, a linked or ordinary direct signal becomes an Exit event. An admitted direct Kill terminates with Killed even when trapping is enabled. A locally chosen or linked Kill is an ordinary reason: it can be trapped and does not force cleanup suppression. This distinction also applies to self-signalling.

Unlink invalidates still-pending effects from earlier link registrations. It preserves an Exit already queued in the mailbox and does not cancel independent direct signals. A new link has a new registration, so cancelling an older one cannot remove the replacement. A retiring actor publishes its linked exits before its monitor completions, including when publication spans multiple bounded scheduler turns.

Process.exit evaluates its reason before committing termination. It does not execute later operands, statements or return continuations. Earlier Result handling obligations still apply; unreachable handling code cannot satisfy them. Ordinary termination runs registered cleanup once. The first committed terminal reason remains authoritative. A checked callback or cleanup fault is recorded before later signals are processed, so those signals cannot replace its Fault reason. Checked failures still follow isolated or legacy supervision policy; an explicit Process.exit(Process.Fault(code)) remains an explicit exit. A later admitted direct Kill can suppress cleanup callbacks that have not begun, while preserving the first reason. A callback or defer already executing must return before retirement proceeds; native calls are not interrupted. New user messages to an exiting actor are rejected. Monitors and links registered before final retirement observe its committed cause.

The link fixture and terminal evaluation fixture provide executable examples; the native matrix verifies exact traces at one, two and four schedulers with stealing off and on.

Cancellation, lifetime and bounds

Process.DemonitorOptions(flush, info) controls cancellation. Without info, a valid owner cancellation returns true. With info, true means the registration was cancelled before its Down was queued; an inactive or consumed monitor returns false. flush also removes an already queued Down for that reference. Combining flush and info does not change an inactive result to true. An already queued Down stays reserved and receivable when cancellation omits flush. Monitoring oneself creates a fresh inert reference; its info result is false.

Errors distinguish ResourceLimit, ForeignInvocation, WrongMonitorOwner, UnsupportedTarget and InvalidOptions. Host ports are not monitor targets. Each actor can hold 256 directed lifecycle reservations, with 4,096 across one invocation. Monitors, link directions and direct exit effects share these limits; a link reserves capacity at both recipients. A future monitor or link completion reserves 4,609 logical bytes: 512 for the control effect and 4,097 for a possible NUL-terminated reason string. A direct signal with a known reason reserves 512 plus its actual text size. Unused future-text credit is released when a reason is materialized. Immutable reason text retained during transport has its own bounded charge. All logical charges share the existing 64 MiB invocation budget; physical metadata accounting is separate.

ShutdownDetail and Failure text is limited to 4,096 UTF-8 bytes. Invalid or oversized direct-signal options return InvalidOptions; unavailable admission returns ResourceLimit, including for direct Kill. An oversized or unadmitted local exit becomes checked Fault(9). Malformed native ABI values remain infrastructure failures. There is no reserved priority lane for Kill.

Reservations remain charged through pending and queued notifications, then release on consumption, cancellation or owner retirement. These slots are separate from the ordinary 4,096-message mailbox limit, so a full user mailbox cannot discard an admitted Down or Exit. Ordinary messages keep their historical 32-byte logical header charge independently of lifecycle metadata size.

A live isolated process may wait indefinitely after main returns. Legacy-only programs retain their existing deadlock behavior. Native embedders can obtain an owned cancellation token and request cancellation from another thread; they must not share a borrowed Exec or operate another scheduler's heap. Cancellation wakes parked drivers and stops the invocation at safe points. Synchronous native calls remain cooperative and cannot be forcibly interrupted.

These APIs are native-only; the REPL rejects their use. Distribution, arbitrary Erlang reason terms, hot code loading and the wider OTP libraries remain outside this local process effort. Pinned OTP reference cases document the external semantic comparisons separately from Morrow's tests and actor performance measurements.