Message Passing & Actors


Eta’s primary concurrency model is now a BEAM-like actor runtime: actors are addressed by PIDs, each actor owns a VM-level mailbox, and failures are represented as ordinary data through exits, links, monitors, and supervisor restart policies.

NNG is still part of the stack, but it is no longer the local actor mailbox abstraction. Use:

LayerUse it for
std.actorLocal PID/mailbox actors, selective receive, links, monitors, exits, registry, introspection.
std.actor.supervisorOTP-style child specs, one-for-one, one-for-all, rest-for-one, restart intensity.
std.actor.gen_serverOTP-style server behaviour with call, cast, callback state, and termination hooks.
std.actor.nodeDistributed node handshakes, remote actor routing, node monitors, and remote process monitor notifications over NNG.
std.netExplicit NNG socket workflows: REQ/REP, PUB/SUB, survey, raw endpoints, and legacy socket-mailbox compatibility.

Actor model overview

A local actor is a lightweight runtime process with:

(import std.actor std.io)

(define worker
  (spawn
    (lambda ()
      (let loop ()
        (receive
          (list
            (match-case (match-list 'ping 1)
                        (lambda (msg)
                          (send (car (cdr msg)) 'pong)
                          (loop)))
            (match-case (match-list 'double 2)
                        (lambda (msg)
                          (let ((reply-to (car (cdr msg)))
                                (n (car (cdr (cdr msg)))))
                            (send reply-to (list 'result (* n 2)))
                            (loop))))
            (match-case (match-symbol 'stop)
                        (lambda (msg) 'ok)))
          'wait)))))

(send worker (list 'ping (self)))
(println (receive-after 1000))        ; => pong

(send worker (list 'double (self) 21))
(println (receive-after 1000))        ; => (result 42)

(send worker 'stop)

Selective receive

receive takes a list of (match-case matcher handler) clauses. The runtime returns the first queued message matching any clause. Messages that do not match stay in the mailbox for later receives.

(receive
  (list
    (match-case (match-list 'ready 1)
                (lambda (msg) (car (cdr msg))))
    (match-case (match-predicate number?)
                (lambda (n) (* n 2))))
  1000
  (lambda () 'timeout))

Helpers in std.actor:

HelperPurpose
match-casePair one matcher with one handler.
match-listMatch list messages by head value and optional tail arity.
match-symbolMatch one symbol by eq?.
match-predicateLift any predicate into a receive matcher.
receive-matchReceive by one predicate or a clause list.
receive-afterReceive one message with a timeout; returns #f on timeout.

Failure semantics

Actor failures follow BEAM-style conventions, expressed in Eta data:

(import std.actor)

(trap-exit! #t)

(define child
  (spawn
    (lambda ()
      (receive-after 'wait))))

(link child)
(exit child '(error boom))

(receive
  (list
    (match-case (match-list 'EXIT 2)
                (lambda (msg) msg)))
  1000
  (lambda () 'timeout))
;; => (EXIT #<pid ...> (error boom))

Monitors are one-way and deliver exactly one DOWN-shaped notification for that monitor reference:

(define doomed (spawn (lambda () 'done)))
(define ref (monitor doomed))

(receive
  (list
    (match-case (match-list 'DOWN 4)
                (lambda (msg) msg)))
  1000
  (lambda () 'timeout))
;; => (DOWN ref process #<pid ...> normal)

Use demonitor with #t to remove a monitor and flush any queued stale notification for that reference.

Supervision

std.actor.supervisor packages the usual “let it crash” pattern with child specs, restart policies, and restart intensity gates.

(import std.actor.supervisor std.actor)

(define specs
  (list
    (child-spec 'worker
                (lambda ()
                  (receive-after 'wait))
                'restart 'permanent
                'shutdown 5000
                'type 'worker)))

(define sup (one-for-one specs 'max-restarts 3 'max-seconds 5))
(supervisor-which-children sup)
(supervisor-count-children sup)

Supported strategies:

StrategyBehaviour
one-for-oneRestart only the failed child.
one-for-allStop and restart every child when one fails.
rest-for-oneStop and restart the failed child and children started after it.

std.supervisor remains as a compatibility shim that re-exports std.actor.supervisor; new code should import std.actor.supervisor.

Gen server behaviour

std.actor.gen_server provides an OTP-style server loop with callback state, synchronous calls, asynchronous casts, and controlled stop/terminate handling.

(import std.actor.gen_server)

(define callbacks
  (list
    (cons 'init (lambda (initial) (list 'ok initial)))
    (cons 'handle-call
          (lambda (request from state)
            (if (eq? request 'get)
                (list 'reply state state)
                (list 'reply 'unknown state))))
    (cons 'handle-cast
          (lambda (message state)
            (if (and (pair? message) (eq? (car message) 'inc))
                (list 'noreply (+ state (car (cdr message))))
                (list 'noreply state))))))

(define server (gen-server-start callbacks 0 '(name counter)))
(gen-server-cast 'counter '(inc 3))
(gen-server-call 'counter 'get 1000)      ; => 3
(gen-server-stop 'counter 'shutdown 1000) ; => ok

Distributed actor nodes

std.actor.node uses NNG as the node-to-node transport bridge. It manages node identity, cookie handshakes, connected-node listing, node monitors, remote actor routing, and netsplit-style monitor notifications.

(import std.actor.node)

(node-listen "tcp://127.0.0.1:7010" 'node-name 'alpha 'cookie 'secret)
(node-connect "tcp://127.0.0.1:7010" 'node-name 'beta 'cookie 'secret)
(nodes)

Node monitor messages use these shapes:

'(nodeup ref node-name node-id)
'(nodedown ref node-name reason)

Remote process monitors use the same DOWN shape as local monitors. Node loss reports remote process DOWN with reason 'noconnection; rejected cookie handshakes report node monitor nodedown with reason 'bad-cookie.

Runtime scheduler controls

The actor scheduler defaults to the pool scheduler. Runtime controls:

VariableMeaning
`ETA_ACTOR_SCHEDULER=thread-per-actorpool
ETA_ACTOR_REDUCTION_BUDGET=<int>Reduction budget before yielding; default 2000.
ETA_ACTOR_DIRTY_SCHEDULERS=<int>Dirty scheduler count for blocking native work; default 0.

Where std.net fits

Use std.net when you explicitly want sockets and endpoint-level protocols:

Do not use std.net as the primary local actor API. For actor code, prefer std.actor and send to PIDs or registered names.

See also