Skip to content

Distribution & Clustering

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 actors
send some_actor inc()

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.

Nodes discover each other via a gossip protocol:

  1. Seeds — A node joins a cluster by connecting to one or more seed nodes
  2. Heartbeats — Nodes periodically heartbeat all known members
  3. Gossip — Membership state propagates transitively through connected peers

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.

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.

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.

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 with delta-state replication and periodic full syncs (every 16 rounds). There are currently no language-level Crdt.* built-in effects.

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]
}
}