swactor-development-history/distribution/distribution_plan.md

353 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```