|
| 1 | +# Cluster Quickstart & Write-Path Verification |
| 2 | + |
| 3 | +This guide shows how to run Kiwi in standalone and Raft cluster modes, and how to |
| 4 | +manually verify the Raft write path (leader writes replicate to followers). |
| 5 | + |
| 6 | +> Automated multi-node integration tests are tracked separately. The steps below |
| 7 | +> are the manual procedure used to validate the write-path integration. |
| 8 | +
|
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +- Rust toolchain (stable) and `protoc` (see the project README / `CLAUDE.md`). |
| 12 | +- `redis-cli` — to drive the RESP port (`brew install redis` / `apt install redis-tools`). |
| 13 | +- `grpcurl` — to call the Raft admin gRPC API for cluster init (`brew install grpcurl`). |
| 14 | + The Raft gRPC server has reflection enabled, so no `.proto` files are needed. |
| 15 | + |
| 16 | +Build the server binary: |
| 17 | + |
| 18 | +```bash |
| 19 | +cargo build --release --bin kiwi # binary at target/release/kiwi |
| 20 | +``` |
| 21 | + |
| 22 | +## Standalone mode (default) |
| 23 | + |
| 24 | +With no Raft configuration, Kiwi runs as a single standalone node. Writes go |
| 25 | +directly to local RocksDB (no consensus). |
| 26 | + |
| 27 | +```bash |
| 28 | +cargo run --release --bin kiwi # listens on 127.0.0.1:7379 by default |
| 29 | +redis-cli -p 7379 set k1 hello # OK |
| 30 | +redis-cli -p 7379 get k1 # "hello" |
| 31 | +``` |
| 32 | + |
| 33 | +## Cluster mode |
| 34 | + |
| 35 | +A node runs in cluster mode when its config file contains the Raft keys below. |
| 36 | +Config files use the Redis-style `key value` format (one per line). |
| 37 | + |
| 38 | +### 1. Write a config per node |
| 39 | + |
| 40 | +`node1.conf`: |
| 41 | + |
| 42 | +```conf |
| 43 | +port 7401 |
| 44 | +binding 127.0.0.1 |
| 45 | +db-path /tmp/kiwi/n1/db |
| 46 | +raft-node-id 1 |
| 47 | +raft-addr 127.0.0.1:8501 |
| 48 | +raft-resp-addr 127.0.0.1:7401 |
| 49 | +raft-data-dir /tmp/kiwi/n1/raft |
| 50 | +raft-use-memory-log-store true |
| 51 | +``` |
| 52 | + |
| 53 | +Create `node2.conf` and `node3.conf` likewise, changing `port`/`raft-addr`/ |
| 54 | +`raft-resp-addr`/`db-path`/`raft-data-dir` and `raft-node-id` (2 and 3). Keep |
| 55 | +`raft-resp-addr` equal to `<binding>:<port>` for each node — it is the address |
| 56 | +returned to clients on redirect. |
| 57 | + |
| 58 | +Raft config keys: |
| 59 | + |
| 60 | +| Key | Meaning | |
| 61 | +|-----|---------| |
| 62 | +| `raft-node-id` | Unique node id (`u64`) | |
| 63 | +| `raft-addr` | gRPC address for Raft internal traffic | |
| 64 | +| `raft-resp-addr` | RESP address advertised to clients (used in `MOVED`) | |
| 65 | +| `raft-data-dir` | Directory for Raft logs / snapshots | |
| 66 | +| `raft-use-memory-log-store` | `true` = in-memory log (testing); `false` = RocksDB-backed | |
| 67 | + |
| 68 | +### 2. Start the nodes |
| 69 | + |
| 70 | +```bash |
| 71 | +RUST_LOG=info ./target/release/kiwi --config node1.conf & |
| 72 | +RUST_LOG=info ./target/release/kiwi --config node2.conf & |
| 73 | +RUST_LOG=info ./target/release/kiwi --config node3.conf & |
| 74 | +``` |
| 75 | + |
| 76 | +Each node now listens on its RESP port and its Raft gRPC port. Until the cluster |
| 77 | +is initialized there is no leader, so **writes are rejected** with `ERR not leader`. |
| 78 | + |
| 79 | +### 3. Initialize the cluster |
| 80 | + |
| 81 | +Call `Initialize` once, on any node's Raft gRPC port, listing all members: |
| 82 | + |
| 83 | +```bash |
| 84 | +grpcurl -plaintext -d '{"nodes":[ |
| 85 | + {"node_id":1,"raft_addr":"127.0.0.1:8501","resp_addr":"127.0.0.1:7401"}, |
| 86 | + {"node_id":2,"raft_addr":"127.0.0.1:8502","resp_addr":"127.0.0.1:7402"}, |
| 87 | + {"node_id":3,"raft_addr":"127.0.0.1:8503","resp_addr":"127.0.0.1:7403"} |
| 88 | +]}' 127.0.0.1:8501 kiwi.raft.v1.RaftAdminService/Initialize |
| 89 | +# => { "response": { "success": true, "message": "OK" }, "leaderId": "1" } |
| 90 | +``` |
| 91 | + |
| 92 | +A leader is elected within the election-timeout window (a second or two). |
| 93 | + |
| 94 | +## Verify the write path |
| 95 | + |
| 96 | +### Find the leader |
| 97 | + |
| 98 | +A write returns `OK` on the leader and `MOVED <leader-resp-addr>` on followers: |
| 99 | + |
| 100 | +```bash |
| 101 | +redis-cli -p 7401 set probe v # OK -> node1 is leader |
| 102 | +redis-cli -p 7402 set probe v # MOVED 127.0.0.1:7401 |
| 103 | +redis-cli -p 7403 set probe v # MOVED 127.0.0.1:7401 |
| 104 | +``` |
| 105 | + |
| 106 | +### Leader write → follower read (replication) |
| 107 | + |
| 108 | +```bash |
| 109 | +# Write on the leader |
| 110 | +redis-cli -p 7401 set repltest hello_from_leader # OK |
| 111 | +redis-cli -p 7401 incr counter # 1 |
| 112 | + |
| 113 | +# Read from a follower (eventually consistent — allow replication to arrive) |
| 114 | +sleep 1 |
| 115 | +redis-cli -p 7402 get repltest # "hello_from_leader" |
| 116 | +redis-cli -p 7403 get repltest # "hello_from_leader" |
| 117 | +redis-cli -p 7402 get counter # "1" |
| 118 | +``` |
| 119 | + |
| 120 | +What this exercises end to end: command → leader gate (passes on leader) → |
| 121 | +`BinlogBatch` captures the encoded CF mutations → Raft `client_write` (consensus) |
| 122 | +→ each node's state machine applies via `on_binlog_write` to local RocksDB → |
| 123 | +follower local reads observe the replicated value. |
| 124 | + |
| 125 | +## Notes & current limitations |
| 126 | + |
| 127 | +- **Reads are eventually consistent.** Followers serve local reads; a value is |
| 128 | + visible only after the entry has replicated and applied. Linearizable reads |
| 129 | + (a read barrier) are not implemented yet. |
| 130 | +- **`MOVED` is simplified.** Kiwi returns `MOVED <addr>` (no hash slot), unlike |
| 131 | + Redis Cluster's `MOVED <slot> <ip:port>`. Off-the-shelf cluster-aware clients |
| 132 | + will not auto-follow it; reconnect to the returned address directly. |
| 133 | +- **Snapshot install.** After a Raft snapshot install, a node's storage is |
| 134 | + swapped and currently does not re-arm the Raft write hook — writes on that node |
| 135 | + would bypass consensus until restarted. This is a known follow-up to fix before |
| 136 | + production cluster use. |
| 137 | +- **Writes only on the leader.** Followers reject writes with `MOVED` / `ERR not |
| 138 | + leader`. Reads are accepted on any node. |
0 commit comments