Actors, ActorRefs, and Actor Groups
Canonical source:
docs/guide/10-actors-actorrefs-and-groups.mdat locked revisione99b712ecf658a527b1fe82c8beba2c1286d8dc9. This rendered page is non-normative; the applicable Protos specification remains authoritative.
Status: Non-normative programming guide
Primary normative owners:
spec/concurrency/ACTORS.mdandspec/concurrency/DISTRIBUTED_RUNTIME.mdRelated normative owners:
spec/semantics/MODULES.md,spec/concurrency/FUTURES_AND_TASKS.md,spec/concurrency/PARALLEL_EXECUTION.md, andspec/semantics/ERRORS.md
Actors are Protos’s persistent isolation boundary for mutable application state.
The essential mental model is:
An Actor owns a private mutable object graph and serialized Protos execution. Other Actors do not call into that graph directly. They communicate through explicit
ActorRefcapabilities and value-transfer boundaries.
Actor Groups add a different abstraction:
A Group has a stable identity independent of its current members. A
GroupRefis a communication capability to that Group, and each ordinary Group message is routed to one eligible concrete Actor member.
Neither abstraction introduces a second object model. Actor behavior is still an ordinary Protos object with ordinary slots, Closures, lookup, and dispatch.
Future, P, Actor, and Group solve different problems
Section titled “Future, P, Actor, and Group solve different problems”The previous chapters introduced Future/task concurrency and isolated P parallelism. Actors add persistent state and identity.
A useful comparison is:
| Mechanism | Mutable state domain | Identity/lifetime | Communication model | Typical use |
|---|---|---|---|---|
| ordinary call | current execution | call/Closure lifetime | ordinary invocation | synchronous local work |
closure.future() |
current Actor | Future/task lifetime | result Future | Actor-local async work |
closure.parallel(...) |
isolated P domain | Future/task lifetime | transferred inputs/result Future | CPU-parallel isolated work |
| Actor | private Actor domain | concrete Actor incarnation | ActorRef.send/request |
persistent isolated mutable state |
| Actor Group | member Actors | stable Group identity | GroupRef.send/request routes to one eligible member |
service/capacity indirection |
A Future is not an Actor merely because it lives for a while.
A P computation is not an Actor merely because it runs in isolation.
An Actor adds a persistent private state domain, mailbox, lifecycle, current behavior, and communication capability.
A Group adds stable routing identity across a changing eligible-member set. It is not a shared mutable object and it is not a broadcast channel.
The Actor behavior is an ordinary Protos object
Section titled “The Actor behavior is an ordinary Protos object”The runtime Actor domain contains:
private object graphmutable Actor-local statecurrent behavior objectmailbox / delivery stateFutures and taskslifecycle stateThe behavior itself is an ordinary Protos object.
When an accepted message is dispatched, ordinary Protos lookup and invocation run against that behavior.
This means familiar object-language rules remain useful inside the Actor:
delegation, this, slots, Closures, Error handling, modules, and Futures do not
become a separate “Actor language”.
The Actor domain is the isolation/lifecycle container around those ordinary objects.
Actor.current() gives the current communication capability
Section titled “Actor.current() gives the current communication capability”Inside Actor execution:
me: Actor.current()returns the ActorRef for the current concrete Actor incarnation.
Repeated observations identify the same incarnation.
This is useful when another Actor must be given an explicit capability back to the current Actor.
It is not equivalent to exposing the Actor’s private heap.
An ActorRef is an opaque communication capability, not a mutable handle to the
target’s internal behavior object.
Runnable companion:
Actor.spawn(...) starts destination-owned code
Section titled “Actor.spawn(...) starts destination-owned code”The standard creation form identifies destination-loadable module code, a bootstrap binding, and initialization values.
For example, the shipped tutorial uses:
worker: Actor.spawn("std:collections/Set", "call", 1, 2, 3)The important boundary is what does not happen:
caller Closure + caller lexical context X | v destination ActorActor creation does not transport a caller Closure with its lexical context, return home, dynamic handlers, or mutable caller state intact.
Instead, the destination Actor loads its own module code and receives only the explicit initialization values permitted by Actor transfer semantics.
Runnable companion:
READY means the Actor has a valid behavior
Section titled “READY means the Actor has a valid behavior”A newly created Actor may pass through initialization before it can dispatch external messages.
The important lifecycle cutover is:
INITIALIZING | | successful initialization + valid behavior vREADYA READY Actor must have one valid current behavior object.
External messages are not dispatched against an incomplete bootstrap behavior while the Actor is still INITIALIZING.
If initialization fails before the READY cutover, that incarnation never becomes READY.
This keeps the programmer-visible model simple: accepted work waits for a real behavior rather than observing a partially constructed one.
Creation genealogy is not authority
Section titled “Creation genealogy is not authority”If Actor A creates Actor B, A receives B’s ActorRef.
B does not automatically receive a capability back to A.
Core defines no ambient:
parentActorcreator()parentRefimplicit reply-to creatorIf B needs to communicate with A, A must pass a capability explicitly, for example as an initialization argument:
selfRef: Actor.current()child: Actor.spawn(moduleName, bootstrapName, selfRef)The transferred ActorRef is then an ordinary explicitly provisioned
capability.
This rule matters for authority reasoning:
creation history does not silently grant communication authority.
Likewise, termination of a creator does not automatically terminate a child merely because of genealogy.
Actor identity is incarnation identity
Section titled “Actor identity is incarnation identity”An Actor has the identity of one concrete incarnation.
An ActorRef permanently identifies exactly that incarnation.
Conceptually:
ActorRef A ───────> incarnation A
incarnation A terminates
ActorRef A ───────> terminated incarnation A (never retargeted)
replacement Actor:ActorRef B ───────> incarnation BA replacement is a new Actor identity with a new ActorRef.
Restarting implementation machinery must not make a dead incarnation appear to have continued under the old identity.
Durable state restored into a replacement does not resurrect the old Actor.
This is why a direct ActorRef is appropriate when the program means “this
specific Actor”, while a Group is the higher-level tool when stable service
routing across membership changes is desired.
Actor messages cross a snapshot boundary
Section titled “Actor messages cross a snapshot boundary”Actors do not share arbitrary mutable Protos object identity.
When values cross an Actor boundary, the runtime performs the defined logical value transfer/snapshot.
For ordinary transferable mutable data, think:
sender graph | | validate + logical snapshot vdestination-local graphThe destination can mutate its transferred graph without thereby mutating the sender’s original graph.
The transfer preserves graph structure that matters inside the transferred snapshot, including aliases and cycles, rather than flattening every edge into unrelated values.
Implementation techniques such as copy-on-write, immutable backing, shared memory, or zero-copy transport are allowed only when they remain observationally equivalent to this logical isolation.
The message snapshot is established synchronously
Section titled “The message snapshot is established synchronously”For standard Actor communication, the caller does not hand the runtime a live mutable graph that may be sampled later at an arbitrary scheduling time.
The complete transferable argument graph is logically captured at the message operation’s defined snapshot point before the call successfully returns its communication result.
This means code can reason about:
payload: someMutableObjectoperation: target.send("consume", payload)
// Later mutation does not retroactively rewrite the already-captured message.mutate(payload)The destination observes the transferred message snapshot, not whichever version happens to exist when a carrier eventually dispatches the handler.
Capabilities are transferred by their own contracts
Section titled “Capabilities are transferred by their own contracts”Ordinary mutable objects are copied/snapshotted across Actors.
Some capability values have explicit cross-Actor semantics.
ActorRef and GroupRef are important examples: crossing the Actor boundary
rematerializes the capability in the destination while preserving the semantic
reference identity that its contract defines.
That does not expose the referenced Actor or Group’s private implementation.
It means the destination receives authority to communicate according to the same capability contract.
Physical wrapper identity is not the semantic identity rule.
Non-transferable resources fail instead of auto-proxying
Section titled “Non-transferable resources fail instead of auto-proxying”Open files, live streams, native/resource handles, and other values without an Actor-transfer contract cannot silently cross just because the runtime could invent some host-level workaround.
Attempting to transfer such a value fails with NonTransferableValue.
The transfer is not partially completed.
The runtime must not silently substitute:
remote proxyreopened fileduplicated OS handlebroker referencenew service capabilityfor the original value.
Those would be new capabilities with potentially different authority, identity, ordering, lifetime, failure, cancellation, and resource-position semantics.
If an application needs a resource service, an explicit Actor/service capability can be designed around that resource. That is different from pretending the original live resource was transferable.
send(...) is asynchronous one-way communication
Section titled “send(...) is asynchronous one-way communication”The public shape is:
target.send(selector, arguments...)The selector names behavior to dispatch at the destination.
send does not return the handler’s ordinary result.
It returns a local SendOperation describing the communication operation.
That distinction is important:
handler result -> ignored by sendcommunication state -> represented by SendOperationUse send when the protocol is intentionally one-way.
SendOperation makes backpressure and retry explicit
Section titled “SendOperation makes backpressure and retry explicit”Actor mailboxes and the complete delivery path are bounded.
They are not conceptually infinite queues.
When capacity is unavailable, communication enters backpressure instead of:
blocking an OS thread forevergrowing an unbounded mailboxsilently dropping the messageThe SendOperation is the local operation object through which the standard
send cancellation/retry behavior is expressed.
Cancellation only claims success when it establishes known cancellation before the concrete acceptance boundary.
It does not pretend to undo already accepted work.
Retry is explicit new communication behavior; the runtime does not silently replay accepted operations.
request(...) returns a fresh Future
Section titled “request(...) returns a fresh Future”Use request/reply when the caller needs one normal reply:
future: target.request("size")value: future.value()Every request returns a fresh Future in the caller’s execution domain.
The destination handler’s normal result crosses the Actor transfer boundary back to the requester.
The reply therefore has the same isolation discipline as message arguments.
Runnable companion:
Request replies do not flatten returned Futures
Section titled “Request replies do not flatten returned Futures”If the destination handler normally returns a Future value, Actor request does not silently turn that remote/internal Future into an adopted continuation of the requester.
The reply-transfer contract remains the authority.
A transferable reply value crosses as defined.
A non-transferable reply fails under the Actor transfer rules.
This avoids making ordinary request/reply a hidden distributed task-adoption protocol.
Same sender to the same concrete Actor has FIFO
Section titled “Same sender to the same concrete Actor has FIFO”Actor communication preserves the defined same-sender ordering to one concrete destination.
The shipped lifecycle tutorial demonstrates the useful case:
worker.send("atPut", "ready", true)print(worker.request("size").value())Both operations are issued by the same Actor to the same concrete Actor, so the later request cannot overtake the earlier send.
Runnable companion:
Do not generalize this into a global ordering guarantee among unrelated senders, Actors, Groups, or destinations.
Acceptance is the critical delivery boundary
Section titled “Acceptance is the critical delivery boundary”Actor messaging distinguishes:
not yet accepted by a concrete Actoraccepted by a concrete Actorhandler startedhandler completedAcceptance means the concrete Actor has taken ownership of the operation.
Before that cutover, cancellation or Group routing may still have outcomes defined for pre-acceptance work.
After acceptance, Protos does not transparently replay the operation against a replacement merely because the Actor later fails.
Why?
Because the handler may already have produced effects.
Automatic replay could duplicate them.
The safe rule is:
accepted work is never silently replayed as though acceptance proved no effect occurred.
Unknown delivery is not known non-delivery
Section titled “Unknown delivery is not known non-delivery”Distributed communication can encounter uncertainty.
If the runtime cannot prove whether the concrete acceptance boundary was crossed, the state is not equivalent to “the message did not happen”.
For request/reply, accepted work lost before a normal reply produces
RequestOutcomeUncertain under the standard contract.
This does not authorize automatic retry.
The application can decide whether a higher-level operation is idempotent, deduplicated, transactional, or safe to retry.
Core does not guess.
Destination Errors are not implicit request replies
Section titled “Destination Errors are not implicit request replies”An Error handled inside an Actor turn is ordinary local control flow.
If an Error escapes a destination Actor turn unhandled, it is a fatal failure of that Actor incarnation under the Core Actor rules.
For an already accepted request, that destination failure does not serialize the private Error object as an implicit reply to the requester.
The sender-visible outcome remains governed by the request acceptance/uncertainty contract.
This separates:
destination internal failure semanticsfrom:
sender communication outcome semanticsDynamic Error handlers never cross Actor boundaries
Section titled “Dynamic Error handlers never cross Actor boundaries”A caller’s active dynamic handlers are execution-local control state.
They are not sent to the destination.
Likewise, destination handler frames are not sent back to the caller.
When a failed Future later re-signals an Error through value(), matching occurs
in the consumer’s then-current dynamic context.
This is consistent with the broader task rule: dynamic handler frames are not ambient Actor-global state.
Non-local return never crosses an Actor boundary
Section titled “Non-local return never crosses an Actor boundary”A ^ return is tied to a return home in one execution structure.
Actor messaging does not transfer:
call stackscontinuationsreturn homeslexical control authoritydynamic execution contextsDestination code may use its own valid non-local returns.
It cannot return into the sender’s suspended call, creator activation, or a dead Actor incarnation.
A request reply is an explicit value-transfer protocol, not a disguised remote stack unwind.
Actor-local work is cooperatively serialized
Section titled “Actor-local work is cooperatively serialized”Inside one Actor, ordinary Protos execution remains serialized.
Actor-local tasks created with future() can interleave only at defined
suspension/cancellation-aware boundaries.
A long CPU-bound segment that never reaches such a boundary can monopolize that Actor domain.
Core does not invent hidden semantic preemption just because the runtime has multiple host threads.
If CPU work should execute independently, use the isolated P boundary described in chapter 09.
This separation preserves the simple Actor invariant:
no two ordinary Protos execution segments simultaneously mutate the same Actor-local object graph.
stop() requests graceful termination
Section titled “stop() requests graceful termination”The standard public lifecycle request is:
actorRef.stop()It returns canonical null.
Repeated calls for the same terminating or already terminated incarnation are idempotent.
Core does not turn stop() into:
a stop Futurea timeout-bearing stopa force-kill controla general administrative Actor handleThe stop cutover makes termination irreversible for that incarnation.
Actor-local tasks and other Actor-owned asynchronous work are cancelled according
to their existing lifecycle rules, including required ensure cleanup.
An already running non-suspending Protos segment is still governed by cooperative execution; graceful stop is not a hidden arbitrary preemption primitive.
termination() observes known termination
Section titled “termination() observes known termination”Lifecycle observation uses the existing Future abstraction:
finished: actorRef.termination()Each call returns a fresh independent non-task-backed Future in the caller’s domain.
When termination of that exact incarnation becomes known, the Future resolves
with that same ActorRef.
If termination is already known when observation is registered, the Future is already resolved accordingly.
Cancelling the observation Future only abandons that observation.
It does not stop the target Actor and it does not cancel other observers.
TERMINATED, UNREACHABLE, and UNKNOWN are different
Section titled “TERMINATED, UNREACHABLE, and UNKNOWN are different”A network failure or lost route does not prove a remote Actor has terminated.
Therefore:
TERMINATED != UNREACHABLE != UNKNOWNActorRef.termination() observes known termination.
A timeout, partition, or transport failure alone cannot falsely resolve that Future as if death were proven.
Replacement also does not retarget an existing observation. A new Actor
incarnation has a new ActorRef and requires a new observation.
Creator lifetime and Actor lifetime are separate
Section titled “Creator lifetime and Actor lifetime are separate”Actor creation is not an implicit parent-child lifetime tree.
If A creates B and A later terminates, B does not automatically terminate solely because A was the creator.
Every Actor belongs to the relevant Process/failure-domain model, but genealogy by itself does not grant communication authority or define automatic child lifetime.
This prevents accidental lifetime coupling from being smuggled in through
spawn.
Fatal failure is per incarnation
Section titled “Fatal failure is per incarnation”Core v0.1 has a fixed baseline failure-authority policy rather than a public configurable supervision API.
For an ordinary non-root Actor:
unhandled fatal Actor failure -> that incarnation terminates -> no automatic Core replacement -> no automatic creator escalation -> no automatic sibling/subtree terminationFor the Process RootActor:
unhandled fatal RootActor failure -> Process terminationHigher-level reconciliation may create another Actor, but that is a new incarnation, not restoration of the dead identity.
There is no public Core supervisor-policy object
Section titled “There is no public Core supervisor-policy object”Core does not expose a mandatory public:
SupervisorFailureAuthorityrestart strategyone-for-one treeone-for-all treefailure policy callbackRuntime machinery still has a well-defined failure authority so failures have a deterministic policy.
A future higher-level supervision abstraction could expose richer policy, but portable Core programs should not assume one exists today.
Group identity solves a different continuity problem
Section titled “Group identity solves a different continuity problem”A Group is not an Actor and not a collection whose identity equals its member list.
Group identity is stable independently of current membership.
Conceptually:
Group Gmembers: A, B
membership changes
Group Gmembers: B, CIt is still Group G.
The Group may even be live with zero eligible members.
That is the key contrast with ActorRef, whose identity is permanently one
concrete Actor incarnation.
Actor.group(...) is the Core Group acquisition surface
Section titled “Actor.group(...) is the Core Group acquisition surface”Core v0.1 provides:
workers: Actor.group(firstMember, additionalMembers...)At least one member is required.
The supplied initial values must denote ActorRefs. The complete member vector
is validated before the Group creation cutover.
Duplicate references to the same Actor incarnation do not create weighted duplicate membership.
A successful invocation synchronously creates:
one fresh Group identityone initial membership setone fresh GroupRef acquisition identityand returns the GroupRef.
Runnable companion:
Group creation does not require ownership of member Actors
Section titled “Group creation does not require ownership of member Actors”Possessing member communication capabilities is sufficient for the Core acquisition operation.
Creating a Group does not:
move membersrestart membersrehost membersmake the caller their supervisormake member lifetimes children of the GroupThe Group is routing/control state over existing Actor identities.
Its lifetime is owned by the caller Actor’s Process under the Core contract.
Group creation does not wait for all members to be READY
Section titled “Group creation does not wait for all members to be READY”Actor.group(...) returns after the Group creation cutover.
It does not need to wait for every supplied member to become READY, reachable, or currently routable.
Routing later uses the then-current eligible member set.
This avoids turning Group acquisition into a cluster-wide barrier.
A GroupRef is a communication capability
Section titled “A GroupRef is a communication capability”The language-visible Core GroupRef communication surface is intentionally
small:
groupRef.send(selector, arguments...)groupRef.request(selector, arguments...)It does not expose general membership/controller authority.
The Group itself is not surfaced as an ordinary mutable Group object through which callers directly edit membership.
Core v0.1 therefore has no standard public post-creation:
group.addMember(...)group.removeMember(...)group.resize(...)group.stop(...)group.members()group.controller()The absence is intentional, not an invitation to infer hidden mutable methods.
A Group message targets one eligible member
Section titled “A Group message targets one eligible member”Ordinary Group send and request are routing operations.
They select one eligible member.
They are not broadcast.
For example, the shipped tutorial says explicitly not to rely on which eligible Actor receives the request:
first: Actor.spawn("std:collections/Set", "call", "Ada", "Grace")second: Actor.spawn("std:collections/Set", "call", "Linus", "Ken")workers: Actor.group(first, second)
print(workers.request("size").value())Runnable companion:
If an application needs multicast or broadcast, that is a different protocol.
Group routing may change only before concrete acceptance
Section titled “Group routing may change only before concrete acceptance”A Group may choose or re-choose a member while an operation remains pre-acceptance.
For example, a selected member may become unavailable before accepting the operation, allowing the Group routing machinery to select another eligible member under the standard rules.
Once a concrete Actor accepts the operation, the Group does not transparently reroute it if that Actor subsequently fails.
The same duplicate-effect argument applies as for direct Actor messaging.
Acceptance turns a Group-routed operation into concrete Actor-owned work.
There is no Group-wide FIFO guarantee
Section titled “There is no Group-wide FIFO guarantee”A Group may route successive messages to different members.
Therefore Core does not promise one total FIFO order over the entire Group.
If two operations from the same sender happen to route to the same concrete Actor, that Actor’s same-sender FIFO rules apply there.
But portable code must not assume:
group.send(A)group.send(B)means every possible member observes A before B.
There may not even be one member that observes both.
A live empty Group can preserve pending routing
Section titled “A live empty Group can preserve pending routing”A Group identity may remain live while no member is currently eligible.
The communication path remains bounded; it does not become an unlimited queue.
Depending on the operation state, routing may remain pending until a member becomes eligible or another standard terminal outcome occurs.
This distinction lets Group identity survive transient capacity changes without pretending zero members means the Group identity vanished.
GroupRef identity is not Group identity
Section titled “GroupRef identity is not Group identity”There are three concepts to keep separate:
Group identityGroupRef semantic identityphysical runtime wrapper/proxyTwo separately acquired GroupRefs can refer to the same Group while remaining
distinct GroupRef identities.
Conversely, transferring/rematerializing the same GroupRef capability across an Actor boundary preserves its semantic GroupRef identity.
The shipped identity tutorial demonstrates an even simpler case: two independent
Actor.group(...) calls over the same member list create different Groups and
different GroupRefs:
Do not infer identity from the current member list.
GroupRefs never retarget
Section titled “GroupRefs never retarget”A concrete GroupRef remains bound to one Group identity.
If a future discovery/name service points a name at some other Group, an already
acquired GroupRef does not silently change target.
This mirrors the ActorRef rule:
ActorRef -> one Actor incarnation foreverGroupRef -> one Group identity foreverThe difference is that Group membership itself can change while Group identity remains stable.
Remote possession does not extend Group lifetime
Section titled “Remote possession does not extend Group lifetime”Transferring a GroupRef to another Actor or Process preserves communication
authority under its contract.
It does not give the holder ownership of the Group’s lifecycle.
Core Group lifetime is owned by the Process that created the Group.
When that owning Process terminates, the Group terminates under the Core lifetime rule.
Keeping another physical wrapper or remote GroupRef alive cannot keep the Group alive beyond that authority boundary.
Group termination does not stop member Actors
Section titled “Group termination does not stop member Actors”The Group and its member Actors have independent identities and lifetimes.
When the Group ends, Core does not thereby stop its member Actors.
Likewise, losing all members does not by itself mean the Group identity has terminated.
This prevents routing topology from silently becoming Actor ownership.
Core v0.1 does not provide portable service discovery
Section titled “Core v0.1 does not provide portable service discovery”A Group is useful as a stable communication/routing capability once acquired.
Core v0.1 does not standardize a public:
service registryglobal Group namelookup-by-namereacquire GroupRef after lossplacement policy APIdurable Group registrypost-creation membership controllerThose are higher-level distributed-runtime concerns.
Portable application code should not pretend Actor.group(...) is a hidden
service-discovery system.
Physical transport and locality are not semantic
Section titled “Physical transport and locality are not semantic”ActorRef and GroupRef communication may be implemented using:
in-process queuesshared memorylocal IPCTCPQUICother conforming transportsThe runtime may even change transport while a logical capability remains live.
Portable semantics must remain unchanged:
same ActorRef/GroupRef identitysame message snapshotsame transferabilitysame acceptance boundarysame FIFO guarantees where applicablesame uncertainty/cancellation behaviorno shared mutable Protos identityCore exposes no portable transport-pinning or locality-query API merely to make those optimizations observable.
Choose direct ActorRef versus GroupRef deliberately
Section titled “Choose direct ActorRef versus GroupRef deliberately”Use a direct ActorRef when the protocol targets one specific incarnation:
this cache workerthis session Actorthis concrete ownerthis lifecycle to monitorUse a GroupRef when the protocol targets a stable service/capacity identity and
any eligible member may process an individual operation:
worker poolstateless service replicassharded/routed capacity where the Group contract is appropriateDo not use Group routing when correctness depends on one particular Actor’s private state unless the higher-level protocol makes that relationship explicit.
Runnable learning path
Section titled “Runnable learning path”The existing executable tutorials are deliberately split so each step introduces one concept at a time.
Actors:
10-actors/01-current-actor.protos— current ActorRef identity.10-actors/02-spawn-and-request.protos— module-backed spawn and request/reply.10-actors/03-send-and-lifecycle.protos— send ordering plus stop/termination observation.
Actor Groups:
11-actor-groups/01-create-group.protos— Group creation.11-actor-groups/02-request-through-group.protos— member-agnostic request routing.11-actor-groups/03-groupref-identity.protos— independent Group/GroupRef identities.
The current language-maturity corpus also retains Actor/Group conformance
coverage under LM005.
Representative implementation evidence
Section titled “Representative implementation evidence”The current implementation ledger records I011 — Actors CLOSED through its
complete incremental runtime/conformance sequence:
That closure covers Actor incarnation identity, ActorRef transfer, module-backed
bootstrap, value transfer, bounded mailboxes, send/request, lifecycle, GroupRef
routing, local/cross-Process/remote uncertainty, and the final
Actor.group(...) acquisition surface.
This implementation record is evidence of what the reference runtime currently supports. It does not override the normative specifications.
Practical rules to remember
Section titled “Practical rules to remember”- An Actor is a persistent isolated mutable state/execution/lifecycle domain; its behavior is an ordinary Protos object.
Actor.current()gives the current incarnation’s communication capability, not direct access to private Actor state.Actor.spawn(...)identifies destination-owned module/bootstrap code and transfers explicit initialization values; it does not transport caller execution context.- Creation genealogy grants no reverse communication capability.
ActorRefidentity is concrete incarnation identity and never retargets.- Replacement always creates a new Actor identity and ActorRef.
- Actor communication snapshots/transfers values; ordinary mutable state is not shared across Actor domains.
ActorRef/GroupRefcross Actor boundaries only through their explicit capability-transfer contracts.- Non-transferable live resources fail with
NonTransferableValue; Core never silently auto-proxies them. sendis one-way and returns aSendOperation; it does not return the destination handler result.- Backpressure is bounded and explicit; mailboxes are not unlimited sinks.
requestreturns a fresh caller-domain Future whose normal reply crosses the Actor value-transfer boundary.- Accepted operations are never transparently replayed against replacements.
- Delivery uncertainty is not proof of non-delivery and does not authorize automatic retry.
- Same-sender FIFO applies to the same concrete Actor, not globally.
- Dynamic Error handlers and non-local return never cross Actor boundaries.
- An unhandled Error escaping a destination turn is fatal to that Actor incarnation; a handled Error is ordinary local control flow.
- Actor-local ordinary Protos work is cooperatively serialized; use P for isolated CPU parallelism.
stop()is an idempotent graceful termination request returningnull, not a force-kill Future.- Each
termination()call creates an independent Future observing known termination of that exact incarnation. - Unreachable/unknown does not equal known terminated.
Actor.group(...)creates one fresh Group and one fresh GroupRef over at least one explicitly supplied ActorRef.- Group identity is independent of current membership.
- A GroupRef is communication authority with standard
send/request, not a public membership/controller handle. - One Group operation routes to one eligible member; ordinary Group messaging is not broadcast.
- Group routing may reselect only before concrete acceptance; accepted work is never transparently rerouted.
- There is no Group-wide FIFO; concrete Actor FIFO applies only when operations route to the same Actor.
- GroupRef identity, Group identity, and physical wrapper identity are different concepts.
- A GroupRef never retargets, and holding one does not extend Group lifetime.
- Group termination does not stop member Actors.
- Core v0.1 does not provide public service discovery, post-creation Group membership control, transport pinning, or topology APIs.
- Choose ActorRef for one concrete incarnation and GroupRef for stable member-agnostic routing authority.
Normative references
Section titled “Normative references”For exact behavior, consult:
../../spec/concurrency/ACTORS.mdfor Actor isolation, behavior, creation, transfer, ActorRef identity, send/request, backpressure, ordering, failure, stop/termination, and Actor-local task semantics;../../spec/concurrency/DISTRIBUTED_RUNTIME.mdfor Group/GroupRef identity, routing, acceptance, lifetime, distributed communication, and the exact CoreActor.group(...)acquisition boundary;../../spec/semantics/MODULES.mdfor Actor-local module identity/cache/bootstrap behavior;../../spec/concurrency/FUTURES_AND_TASKS.mdfor Futures used by request, lifecycle observation, task ownership, and cancellation;../../spec/concurrency/PARALLEL_EXECUTION.mdfor the P boundary contrasted with Actor-local cooperative execution;../../spec/semantics/ERRORS.mdfor ordinary Error handling and the dynamic-control rules reused inside Actors;- chapter 08 for Future observation and structured ownership;
- chapter 09 for isolated CPU parallelism.
Those documents define Protos. This chapter supplies the programmer-facing model for choosing Actor and Group capabilities without importing a shared-memory, implicit-supervision, or transparent-retry model into the language.