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.
actor Counter { behavior inc() { 0 }}let some_actor = spawn Counter {}
// 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”Cluster membership uses heartbeat-based discovery and a gossip protocol for transitive propagation. A node joins by connecting to one or more seed nodes; membership state (Joining, Healthy, Suspicious, Left, Removed) propagates through connected peers. Each membership entry carries an incarnation number — higher incarnations win in merge conflicts, preventing split-brain regressions.
Member States
Section titled “Member States”| State | Description |
|---|---|
Joining |
Initial state, awaiting first gossip round |
Healthy |
Active member, heartbeating and receiving messages |
Suspicious |
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 is accessed via the Rust API: behaviors are registered with Runtime::register_spawnable_behavior, and the wire protocol uses Packet::SpawnRequest/SpawnResponse for cross-node actor creation. The receiver spawns the named behavior only if it was pre-registered; unknown names return a failed response. A language-level remote-spawn expression is planned but not yet implemented.
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.
CRDT types (GCounter, PNCounter, GSet, ORSet, AWORSet, LWWRegister, MVRegister, RGA) are Rust-level APIs on CrdtManager. Actor state fields can also be declared CRDT-backed (state crdt gcounter hits: Int = 0 — the crdt state model takes an optional CRDT type keyword: gcounter, pncounter, gset, orset, aworset, lwwregister, mvregister, rga), and the runtime wires a language-level Crdt.* built-in effect for operating on them:
| Effect op | Signature | Description |
|---|---|---|
Crdt.increment |
increment(field: String) -> Unit |
Increment a gcounter/pncounter field |
Crdt.decrement |
decrement(field: String) -> Unit |
Decrement a pncounter field |
Crdt.add |
add(field: String, item: String) -> Unit |
Add an element to a gset/orset/aworset field |
Crdt.remove |
remove(field: String, item: String) -> Unit |
Remove an element from an orset/aworset field |
Crdt.set |
set(field: String, value: String) -> Unit |
Write a lwwregister/mvregister field |
Crdt.read |
read(field: String) -> Int | String |
Read the materialized value (count for counters, element count for sets/RGA, stored string for registers) |
Each op is rejected (returns nil) when the field’s CRDT type doesn’t match, and also returns nil outside an actor context. See CRDT Stdlib Reference for details.
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.
// State durability is configured at the runtime level// (Local, Durable, EventSourced, Crdt — see PersistenceStore)actor DurableWorker { state tasks: [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