Distribution & Clustering
Location Transparency
Section titled “Location Transparency”Nulang actors are location-transparent: you send to an actor without knowing (or caring) which node it lives on. The runtime resolves actor addresses — local or remote — transparently.
// Same code works for local AND remote actorssend some_actor inc()Actor Addresses
Section titled “Actor Addresses”An ActorAddress is either:
- Local — a direct 64-bit actor id on the current node
- Remote — a
(node_id, actor_id)pair on a different node
The AddressResolver maintains an LRU cache (10k entries) mapping remote addresses.
Cluster Membership
Section titled “Cluster Membership”Nodes discover each other via a gossip protocol:
- Seeds — A node joins a cluster by connecting to one or more seed nodes
- Heartbeats — Nodes periodically heartbeat all known members
- Gossip — Membership state propagates transitively through connected peers
Joining a Cluster
Section titled “Joining a Cluster”// Start a node and join a cluster// Each node needs a unique node_idlet node = spawn ClusterNode { state node_id: Int, state seeds: [String] // e.g. ["192.168.1.10:9000"]} in { ... }Member States
Section titled “Member States”| State | Description |
|---|---|
Joining |
Initial state, awaiting first gossip round |
Healthy |
Active member, heartbeating and receiving messages |
Suspect |
Missed heartbeats, awaiting confirmation |
Left |
Gracefully departed |
Removed |
Pruned from membership after timeout |
Membership state carries an incarnation number — higher incarnations win in merge conflicts, preventing split-brain regressions.
Remote Spawn
Section titled “Remote Spawn”Spawn an actor on a remote node:
// Register a spawnable behavior on the remote node firstperform Actor.register("worker")
// Spawn on a remote nodelet remote_actor = spawn_on_node("10.0.0.5:9000") Worker { state task: String = "process"} in { remote_actor}The remote node must have Worker registered via Runtime::register_spawnable_behavior.
Wire Protocol
Section titled “Wire Protocol”The distribution layer uses a custom TCP protocol:
- Magic:
NUL0(4 bytes) - Handshake: 8-byte node id exchange
- Frames: Length-prefixed, big-endian encoded
- Packet types:
ActorMessage,Heartbeat,Ack,SpawnRequest,SpawnResponse,CrdtSync,CrdtDeltaSync,Gossip
String values travel by content — the sender populates a string table and the receiver interns strings into its module pool. Heap pointers, closures, actor refs, and nil are rejected at send time.
CRDT Replication
Section titled “CRDT Replication”Nulang supports 8 Conflict-free Replicated Data Types for eventually-consistent state:
| CRDT | Description |
|---|---|
GCounter |
Grow-only counter |
PNCounter |
Positive-negative counter (increment/decrement) |
GSet |
Grow-only set |
ORSet |
Observed-remove set |
AWORSet |
Add-wins observed-remove set |
LWWRegister |
Last-writer-wins register |
MVRegister |
Multi-value register |
RGA |
Replicated growable array |
CRDTs use delta-state replication: only changed state (deltas) is shipped over the wire, with periodic full syncs (every 16 rounds) as a repair mechanism.
Using CRDTs
Section titled “Using CRDTs”actor SharedCounter { state counter = Crdt.new_gcounter("my_counter")
behavior inc() { Crdt.increment(self.counter, 1) }
behavior get(sender: Actor) { let value = Crdt.value(self.counter) send sender reply(value) }}Persistence
Section titled “Persistence”Actors can be persisted for durability:
| Store | Description |
|---|---|
MemoryStore |
In-memory (default, ephemeral) |
JsonFileStore |
JSON file on disk |
SqliteStore |
SQLite database (via rusqlite) |
Persistent actors support journaling and checkpointing for crash recovery.
// Actor state model annotationsactor DurableWorker { state tasks: Durable [String] = []
behavior add_task(task: String) { self.tasks = self.tasks + [task] }}- Actor Model Overview — core actor concepts (spawn, send, receive)
- Supervision Trees — OTP supervisors for fault-tolerant clusters
- Standard Library Overview — built-in effects and operations