swactor/docs/development_history/distribution_plan.md

354 lines
16 KiB
Markdown
Raw Normal View History

# Distributed Actor Runtime: Implementation Plan
## Overview
Two-layer distributed system:
1. **SWIM/Lifeguard membership** — node discovery, failure detection, membership gossip.
2. **Kademlia-style actor directory** — decentralized `actor_id → node` lookup with signed entries, quorum reads, Byzantine-tolerant up to `n ≤ f < 2f + 1`.
Key properties: 256-bit actor IDs, fixed placement (no migration), forwarding on cache miss is acceptable, node count << actor count, actor count unbounded.
```
┌──────────────────────────────────────────────────┐
│ Node │
│ │
│ Local Registry ─ LRU Cache ─ Directory Shard │
│ │
│ ──── Kademlia Routing Table (256 k-buckets) ── │
│ │
│ ──── SWIM Membership Layer ─────────────────── │
│ │
│ ──── Transport (pluggable) ─────────────────── │
└──────────────────────────────────────────────────┘
```
### Core Invariants
- `node_id` = ed25519 public key (identity + signing key in one).
- Directory entries are signed by the spawning node. Replication factor `r = 2f+1`, quorum reads require `f+1` agreement.
- SWIM membership is a per-node CRDT: higher generation wins, within a generation `dead > suspect > alive`.
---
## Existing Infrastructure (what we're building on)
### Already implemented
- **`ActorAddress([u8; 32])`** — 256-bit actor identity, random generation, serde support. Lives in `src/actor.rs`.
- **Transport layer** (`src/transport.rs`, feature-gated `transport`):
- `Transport` trait — `fn send(&self, envelope: WireEnvelope) -> Result<(), Error>`
- `Codec<M>` trait — user-provided encode/decode per message type
- `NetworkMessage` trait — marker with `type_tag()` for wire routing
- `WireEnvelope { dest: ActorAddress, type_tag: String, payload: Vec<u8> }`
- `CodecRegistry` — type-erased encoder/decoder dispatch (TypeId → encode, type_tag → decode)
- `TransportRouter` — address→transport mapping (`RwLock<HashMap<ActorAddress, Arc<dyn Transport>>>`)
- `InMemoryTransport` — in-process transport via mpsc
- `send_via_transport()` — crate-internal helper wiring codec+router
- TCP transport example with length-prefix framing in `examples/tcp_ping_pong.rs`
- **Delivery integration** (`src/delivery.rs`):
- `TickContext::route_nonlocal()` — tries inbox registry → transport router → error
- Message routing already falls through to transport when address is not local
- **Gossip crate** (`crates/swactor-gossip/`):
- LWW key-value gossip (NOT SWIM membership — different protocol)
- Full-push gossip (sends entire state each round)
- Simulation harness with topologies: Ring, Star, FullMesh, Chain, Partitioned
- Event tracing, snapshots, property-based tests
- Gossip + runtime dashboards (`crates/gossip-dashboard/`, `crates/runtime-dashboard/`)
### What still needs building
- `NodeId` type (ed25519 public key) — distinct from `ActorAddress`
- ed25519 crypto primitives (keypair gen, sign, verify)
- `DirectoryEntry`, `NodeRecord` types
- SWIM membership protocol (probes, failure detection, dissemination)
- Piggyback field on `WireEnvelope` for SWIM dissemination
- Kademlia routing table and lookup
- Actor directory (STORE / FIND_VALUE with quorum)
- Node-level integration type
- Connection pooling and bidirectional TCP listener
---
## Workflow
Each chunk: **Think** (understand constraints), **Plan** (design interfaces), **Act** (implement and test).
After each chunk: `git add -A && git commit -m "<chunk summary>"`.
---
### Chunk 0: Core Types and Crypto
Define `NodeId` (ed25519 public key wrapper), `Keypair`, `Signature`, `DirectoryEntry`, `NodeRecord`. Leverage existing `ActorAddress` as-is for actor identity. Add `ed25519-dalek` dependency. Implement sign/verify. Unit test serialization round-trips and signature correctness.
**New crate**: `crates/swactor-distribution/` — keeps distribution concerns out of the core runtime.
**Types to define**:
- `NodeId([u8; 32])` — ed25519 public key, XOR distance for Kademlia
- `Keypair` — ed25519 signing key + public key
- `Signature([u8; 64])` — ed25519 signature
- `NodeRecord { node_id, addr: SocketAddr, generation: u64 }` — SWIM membership record
- `DirectoryEntry { actor_addr: ActorAddress, node_id: NodeId, generation: u64, signature: Signature }` — signed actor→node binding
- `MemberState { Alive, Suspect, Dead }` — SWIM state enum
**Files**: `crates/swactor-distribution/src/{lib.rs, types.rs, crypto.rs}`
```bash
git add -A && git commit -m "chunk-0: distribution crate, core types, crypto primitives"
```
---
### Chunk 1: Transport Extensions
Extend the existing transport layer for distribution needs. The `Transport` trait, `Codec`, `WireEnvelope`, `CodecRegistry`, and `TransportRouter` already exist — this chunk adds what's missing for node-to-node communication.
**Changes**:
- Add optional `piggyback: Vec<u8>` field to `WireEnvelope` for SWIM dissemination (backward-compatible: empty vec = no piggyback)
- Promote the TCP transport from the example into a reusable `TcpTransport` in the distribution crate, with connection pooling (`HashMap<SocketAddr, TcpStream>`) and a listening accept loop
- Add `request()` to `Transport` trait (send + await response) — needed for SWIM probes and Kademlia lookups
- Register distribution message codecs (`Ping`, `PingReq`, `Ack`, `FindNode`, `Store`, `FindValue`) in a `DistributionCodecRegistry`
**Files**: `crates/swactor-distribution/src/{transport.rs, codec.rs}`, modifications to `src/transport.rs` (piggyback field)
```bash
git add -A && git commit -m "chunk-1: transport extensions for distribution"
```
---
### Chunk 2: SWIM Probes
Implement the SWIM probe cycle as a state machine in the distribution crate. This is pure protocol logic, testable without networking.
**Components**:
- `SwimProbe` state machine: periodic random-order pinging, `PingReq` indirect probes on timeout
- `MemberList` — the membership CRDT: `HashMap<NodeId, (MemberState, incarnation: u64)>`
- State transitions: `Alive → Suspect → Dead`, with incarnation-based refutation (suspected node bumps incarnation to refute)
- `SwimConfig` — probe interval, probe timeout, suspicion timeout
**Key design**: The probe logic is a pure function `(current_state, event) → (new_state, actions)` where actions are messages to send. This makes it testable without real networking — reuse the simulation pattern from `swactor-gossip`.
**Files**: `crates/swactor-distribution/src/{swim/mod.rs, swim/probe.rs, swim/member_list.rs}`
```bash
git add -A && git commit -m "chunk-2: SWIM probe cycle and failure detection"
```
---
### Chunk 3: SWIM Dissemination
Membership changes piggyback on existing protocol messages — no separate gossip channel. This builds on the `piggyback` field added in Chunk 1.
**Components**:
- Dissemination queue: list of `(MembershipUpdate, transmit_count)` entries
- Infection-style counting: each update transmitted `Λ * log(n)` times before eviction
- Priority ordering: `dead > suspect > alive` (most urgent first)
- Piggyback packing: serialize top-N updates into the piggyback field of outgoing messages
- Piggyback unpacking: on receive, extract and apply membership updates before processing the primary message
**Reuse**: The `swactor-gossip` simulation harness (topologies, tracing) can validate dissemination convergence. Consider adapting the property tests.
**Files**: `crates/swactor-distribution/src/swim/dissemination.rs`
```bash
git add -A && git commit -m "chunk-3: SWIM piggybacked dissemination"
```
---
### Chunk 4: SWIM Join Protocol
Implement seed-node bootstrap and dynamic cluster formation.
**Components**:
- `JoinRequest` / `JoinResponse` messages
- New node contacts seed(s), receives current member list, is announced via dissemination
- Solo-node case: first node starts with empty member list, becomes its own seed
- `SwimNode` — the integrated SWIM actor: probe timer + dissemination + join/leave
**Files**: `crates/swactor-distribution/src/swim/join.rs`, update `swim/mod.rs`
```bash
git add -A && git commit -m "chunk-4: join protocol and seed node bootstrap"
```
---
### Chunk 5: Kademlia Routing Table
Pure data structure, no network calls. Implement as a standalone module.
**Components**:
- 256-entry k-bucket array indexed by `XOR(self_id, target_id).leading_zeros()`
- XOR distance metric on `NodeId` (256-bit)
- Per-bucket LRU eviction: prefer long-lived nodes, new nodes wait in replacement cache
- `closest(target: NodeId, count: usize) -> Vec<NodeId>` — k-closest query
- `insert(node_id)` / `remove(node_id)` with LRU maintenance
**Files**: `crates/swactor-distribution/src/kademlia/routing_table.rs`
```bash
git add -A && git commit -m "chunk-5: kademlia k-bucket routing table"
```
---
### Chunk 6: Kademlia Node Lookup
Iterative `FIND_NODE` using the `Transport::request()` method from Chunk 1.
**Components**:
- `NodeLookup` — async iterative walker: start from α closest local contacts, query in parallel, incorporate responses, converge on k-closest
- `FindNodeRequest { target: NodeId }` / `FindNodeResponse { closest: Vec<(NodeId, SocketAddr)> }` messages
- Lookup termination: all k-closest nodes queried, or max rounds exceeded
**Files**: `crates/swactor-distribution/src/kademlia/lookup.rs`
```bash
git add -A && git commit -m "chunk-6: iterative FIND_NODE lookup"
```
---
### Chunk 7: Actor Directory (STORE / FIND_VALUE)
The largest chunk. Signed directory entries with quorum reads.
**Components**:
- `DirectoryShard` — local storage of `HashMap<ActorAddress, Vec<DirectoryEntry>>`
- **STORE**: sign a `DirectoryEntry`, use FIND_NODE to locate the `r` closest nodes to the `ActorAddress`, store on all of them
- **FIND_VALUE**: quorum read — query `r` nodes, require `f+1` agreement on the same `(node_id, generation)`, verify signatures, highest-generation-wins conflict resolution
- Fallback: if quorum not met from initial `r` nodes, iterative walk to find more replicas
**Files**: `crates/swactor-distribution/src/kademlia/directory.rs`
```bash
git add -A && git commit -m "chunk-7: signed directory STORE and quorum FIND_VALUE"
```
---
### Chunk 8: Cache and Message Routing
Wire the directory into the existing routing pipeline in `src/delivery.rs`.
**Components**:
- LRU cache: `ActorAddress → NodeId` with bounded capacity and TTL
- Extended routing pipeline: local `AddressMap` → LRU cache hit → Kademlia FIND_VALUE → `Transport::send()`
- Redirect/forward on receiving side: if a message arrives for a non-local actor, look up the correct node and forward
- Cache invalidation: on delivery failure (transport error), evict the stale entry and re-resolve
**Integration point**: `TickContext::route_nonlocal()` currently tries inbox → transport. This chunk extends it to: inbox → cache → directory resolve → transport.
**Files**: `crates/swactor-distribution/src/cache.rs`, modifications to `src/delivery.rs`
```bash
git add -A && git commit -m "chunk-8: LRU cache and message routing pipeline"
```
---
### Chunk 9: Directory Republish and Repair
React to SWIM death notifications to maintain directory consistency.
**Components**:
- Wire SWIM `Dead` events into directory layer: when a node dies, identify affected directory entries and replicate to replacement nodes
- Periodic republish: spawning nodes re-STORE their entries on a timer to heal accumulated churn
- TTL-based expiration: entries whose host node is confirmed dead are expired after a grace period
**Files**: `crates/swactor-distribution/src/kademlia/repair.rs`
```bash
git add -A && git commit -m "chunk-9: directory republish and churn repair"
```
---
### Chunk 10: Node Integration
Compose SWIM + Kademlia + Transport + Cache into a single `DistributedNode` type.
**Components**:
- `DistributedNode` — public API: `start(config)`, `stop()`, `spawn(actor)`, `send(addr, msg)`, `members() -> Vec<NodeRecord>`
- Wraps a `Runtime` + `SwimNode` + `RoutingTable` + `DirectoryShard` + `LruCache`
- Startup sequence: generate keypair → bind transport → join cluster (SWIM) → populate routing table → ready
- Shutdown sequence: leave cluster (SWIM disseminate Dead for self) → drain in-flight messages → close transport
- End-to-end test: multi-node cluster, spawn actors, send cross-node messages, kill nodes, verify fault tolerance
**Files**: `crates/swactor-distribution/src/node.rs`, `crates/swactor-distribution/tests/integration.rs`
```bash
git add -A && git commit -m "chunk-10: node integration and public API"
```
---
### Chunk 11: Hardening (Lifeguard)
Add Lifeguard protocol extensions for production resilience.
**Components**:
- **Local Health Multiplier (LHM)**: degraded nodes (high nack rate, slow acks) increase their own probe interval to reduce false accusations
- **Dynamic suspect timeout**: scaled by `log(n)` where n = cluster size
- **Protocol period scaling**: under load, probe intervals stretch rather than dropping probes
- Stress tests: simulated partitions, asymmetric failures, high churn — reuse the `swactor-gossip` simulation harness patterns
**Files**: `crates/swactor-distribution/src/swim/lifeguard.rs`, stress test binaries
```bash
git add -A && git commit -m "chunk-11: lifeguard hardening and stress tests"
```
---
## Dependency Graph
```
[0] ─→ [1] ─→ [2] ─→ [3] ─→ [4] ─┐
│ │
└─→ [5] ─→ [6] ─→ [7] ─┐
├─→ [8] ─→ [10] ─→ [11]
│ │
│ [9] ┘
│
[4] ─────┘
```
Chunks 2-4 (SWIM) and 5-7 (Kademlia) can be developed in parallel off the transport extensions. Chunk 10 merges them. Chunk 11 is a hardening pass.
---
## Crate Layout
```
crates/swactor-distribution/
├── Cargo.toml # deps: swactor, ed25519-dalek, serde
├── src/
│ ├── lib.rs
│ ├── types.rs # NodeId, Keypair, Signature, NodeRecord, DirectoryEntry, MemberState
│ ├── crypto.rs # sign, verify, keypair generation
│ ├── transport.rs # TcpTransport (pooled), DistributionCodecRegistry
│ ├── codec.rs # Codecs for all distribution messages
│ ├── cache.rs # LRU actor location cache
│ ├── node.rs # DistributedNode public API
│ ├── swim/
│ │ ├── mod.rs # SwimNode actor
│ │ ├── probe.rs # Probe cycle state machine
│ │ ├── member_list.rs # Membership CRDT
│ │ ├── dissemination.rs # Piggybacked gossip queue
│ │ ├── join.rs # Seed-node bootstrap
│ │ └── lifeguard.rs # LHM, dynamic timeouts
│ └── kademlia/
│ ├── mod.rs
│ ├── routing_table.rs # k-bucket array
│ ├── lookup.rs # Iterative FIND_NODE
│ ├── directory.rs # STORE / FIND_VALUE with quorum
│ └── repair.rs # Republish and churn healing
└── tests/
└── integration.rs # End-to-end multi-node tests
```