Skip to content

Commit 5ef0eb1

Browse files
author
lupengfan1
committed
Merge remote-tracking branch 'upstream/main' into fix-snapshot-logindex-sync
# Conflicts: # src/storage/src/lib.rs # src/storage/src/redis.rs # src/storage/src/storage.rs
2 parents 3f80182 + e392797 commit 5ef0eb1

19 files changed

Lines changed: 642 additions & 172 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cluster-quickstart.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.

src/cmd/src/lib.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,6 @@ pub trait Cmd: Send + Sync {
232232
fn get_sub_cmd(&self, _cmd_name: &str) -> Option<&dyn Cmd> {
233233
None
234234
}
235-
236-
fn to_binlog(&self, _client: &Client) -> Option<conf::raft_type::Binlog> {
237-
None
238-
}
239-
240-
fn needs_raft(&self) -> bool {
241-
self.has_flag(CmdFlags::RAFT)
242-
}
243235
}
244236

245237
#[macro_export]

src/cmd/src/set.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ use storage::storage::Storage;
2323

2424
use crate::{Cmd, CmdFlags, CmdMeta};
2525
use crate::{impl_cmd_clone_box, impl_cmd_meta};
26-
use conf::raft_type::{Binlog, BinlogEntry, ColumnFamilyIndex as RaftCfIndex, OperateType};
27-
use storage::slot_indexer::key_to_slot_id;
2826

2927
#[derive(Clone, Default)]
3028
pub struct SetCmd {
@@ -74,24 +72,4 @@ impl Cmd for SetCmd {
7472
}
7573
}
7674
}
77-
78-
fn to_binlog(&self, client: &Client) -> Option<Binlog> {
79-
let key = client.key();
80-
let value = &client.argv()[2];
81-
82-
let slot_id = key_to_slot_id(&key) as u32;
83-
84-
let entry = BinlogEntry {
85-
cf_idx: RaftCfIndex::MetaCF as u32,
86-
op_type: OperateType::Put,
87-
key: key.clone(),
88-
value: Some(value.clone()),
89-
};
90-
91-
Some(Binlog {
92-
db_id: 0,
93-
slot_idx: slot_id,
94-
entries: vec![entry],
95-
})
96-
}
9775
}

src/net/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ executor = { path = "../executor" }
2121
runtime = { path = "../common/runtime" }
2222
thiserror = "2.0"
2323
parking_lot = "0.12"
24-
# raft = { path = "../raft" } # removed: src/raft no longer exists
24+
raft = { path = "../raft" }
2525

2626
[dev-dependencies]
2727
env_logger = "0.11"

src/net/src/executor_ext.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::pin::Pin;
2525
use std::sync::Arc;
2626
use std::time::Duration;
2727

28+
use cmd::CmdFlags;
2829
use executor::CmdExecutor;
2930
use log::{debug, error, warn};
3031
use resp::RespData;
@@ -58,6 +59,22 @@ impl CmdExecutorNetworkExt for CmdExecutor {
5859
return Ok(());
5960
}
6061

62+
// Cluster-mode leader gate: reject writes on non-leaders before any
63+
// command-specific setup runs.
64+
if let Some(gate) = exec.leader_gate.as_ref() {
65+
if exec.cmd.has_flag(CmdFlags::WRITE) && !gate.is_leader() {
66+
// Simplified redirect: Kiwi returns "MOVED <addr>" (no hash slot,
67+
// unlike Redis Cluster's "MOVED <slot> <ip:port>"). Clients are
68+
// expected to reconnect to the returned leader address directly.
69+
let reply = match gate.leader_resp_addr() {
70+
Some(addr) => format!("MOVED {addr}"),
71+
None => "ERR not leader".to_string(),
72+
};
73+
exec.client.set_reply(RespData::Error(reply.into()));
74+
return Ok(());
75+
}
76+
}
77+
6178
// Execute do_initial if needed
6279
if !exec.cmd.do_initial(&exec.client) {
6380
debug!("Command initial check failed for: {}", cmd_name);

src/net/src/handle.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,15 @@ pub async fn process_connection_with_storage_client(
129129
storage_client: Arc<StorageClient>,
130130
cmd_table: Arc<CmdTable>,
131131
executor: Arc<CmdExecutor>,
132+
leader_gate: Option<std::sync::Arc<dyn raft::leader_gate::LeaderGate>>,
132133
) -> std::io::Result<()> {
133134
// Delegate to the network-aware connection handler
134-
crate::network_handle::process_network_connection(client, storage_client, cmd_table, executor)
135-
.await
135+
crate::network_handle::process_network_connection(
136+
client,
137+
storage_client,
138+
cmd_table,
139+
executor,
140+
leader_gate,
141+
)
142+
.await
136143
}

src/net/src/lib.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,18 @@ impl ServerFactory {
5757
addr: Option<String>,
5858
runtime_manager: &RuntimeManager,
5959
requirepass: Option<String>,
60+
leader_gate: Option<Arc<dyn raft::leader_gate::LeaderGate>>,
6061
) -> Option<Box<dyn ServerTrait>> {
6162
match protocol.to_lowercase().as_str() {
62-
"tcp" => match Self::create_network_server(addr, runtime_manager, requirepass) {
63-
Ok(server) => Some(Box::new(server) as Box<dyn ServerTrait>),
64-
Err(e) => {
65-
log::error!("Failed to create NetworkServer: {}", e);
66-
None
63+
"tcp" => {
64+
match Self::create_network_server(addr, runtime_manager, requirepass, leader_gate) {
65+
Ok(server) => Some(Box::new(server) as Box<dyn ServerTrait>),
66+
Err(e) => {
67+
log::error!("Failed to create NetworkServer: {}", e);
68+
None
69+
}
6770
}
68-
},
71+
}
6972
#[cfg(unix)]
7073
"unix" => match unix::UnixServer::new(addr, None) {
7174
Ok(server) => Some(Box::new(server) as Box<dyn ServerTrait>),
@@ -113,6 +116,7 @@ impl ServerFactory {
113116
addr: Option<String>,
114117
runtime_manager: &RuntimeManager,
115118
requirepass: Option<String>,
119+
leader_gate: Option<Arc<dyn raft::leader_gate::LeaderGate>>,
116120
) -> Result<NetworkServer, Box<dyn std::error::Error>> {
117121
// Get the storage client from RuntimeManager
118122
let runtime_storage_client = runtime_manager.storage_client().map_err(|e| {
@@ -132,6 +136,13 @@ impl ServerFactory {
132136
})));
133137
let executor = Arc::new(CmdExecutorBuilder::new().build());
134138

135-
NetworkServer::new(addr, storage_client, cmd_table, executor, requirepass)
139+
NetworkServer::new(
140+
addr,
141+
storage_client,
142+
cmd_table,
143+
executor,
144+
requirepass,
145+
leader_gate,
146+
)
136147
}
137148
}

src/net/src/network_execution.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,7 @@ pub struct NetworkCmdExecution {
4040
pub client: Arc<Client>,
4141
/// The storage client for network-to-storage communication
4242
pub storage_client: Arc<StorageClient>,
43-
}
44-
45-
impl NetworkCmdExecution {
46-
/// Create a new NetworkCmdExecution
47-
pub fn new(cmd: Arc<dyn Cmd>, client: Arc<Client>, storage_client: Arc<StorageClient>) -> Self {
48-
Self {
49-
cmd,
50-
client,
51-
storage_client,
52-
}
53-
}
43+
/// Optional leadership gate; `None` in standalone mode. When `Some` and the
44+
/// command is a write on a non-leader, the executor replies `-MOVED` (Task 7).
45+
pub leader_gate: Option<std::sync::Arc<dyn raft::leader_gate::LeaderGate>>,
5446
}

src/net/src/network_handle.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub async fn process_network_connection(
4949
storage_client: Arc<StorageClient>,
5050
cmd_table: Arc<CmdTable>,
5151
executor: Arc<CmdExecutor>,
52+
leader_gate: Option<std::sync::Arc<dyn raft::leader_gate::LeaderGate>>,
5253
) -> std::io::Result<()> {
5354
let mut buf = vec![0; 4096]; // Increased buffer size for better performance
5455
let mut resp_parser = resp::RespParse::new(resp::RespVersion::RESP2);
@@ -71,6 +72,7 @@ pub async fn process_network_connection(
7172
storage_client.clone(),
7273
cmd_table.clone(),
7374
executor.clone(),
75+
leader_gate.clone(),
7476
).await;
7577
}
7678
return Ok(());
@@ -97,6 +99,7 @@ pub async fn process_network_connection(
9799
storage_client.clone(),
98100
cmd_table.clone(),
99101
executor.clone(),
102+
leader_gate.clone(),
100103
).await;
101104
pending_commands.clear();
102105
}
@@ -124,6 +127,7 @@ pub async fn process_network_connection(
124127
storage_client.clone(),
125128
cmd_table.clone(),
126129
executor.clone(),
130+
leader_gate.clone(),
127131
).await;
128132
pending_commands.clear();
129133
}
@@ -151,6 +155,7 @@ async fn handle_network_command(
151155
storage_client: Arc<StorageClient>,
152156
cmd_table: Arc<CmdTable>,
153157
executor: Arc<CmdExecutor>,
158+
leader_gate: Option<std::sync::Arc<dyn raft::leader_gate::LeaderGate>>,
154159
) {
155160
// Convert the command name from &[u8] to a lowercase String for lookup
156161
let cmd_name = String::from_utf8_lossy(&client.cmd_name()).to_lowercase();
@@ -164,6 +169,7 @@ async fn handle_network_command(
164169
cmd: cmd.clone(),
165170
client: client.clone(),
166171
storage_client: storage_client.clone(),
172+
leader_gate: leader_gate.clone(),
167173
};
168174

169175
// Execute the command using the network-aware executor
@@ -340,6 +346,7 @@ async fn process_command_batch(
340346
storage_client: Arc<StorageClient>,
341347
cmd_table: Arc<CmdTable>,
342348
executor: Arc<CmdExecutor>,
349+
leader_gate: Option<std::sync::Arc<dyn raft::leader_gate::LeaderGate>>,
343350
) {
344351
debug!("Processing command batch of {} commands", commands.len());
345352

@@ -371,6 +378,7 @@ async fn process_command_batch(
371378
storage_client.clone(),
372379
cmd_table.clone(),
373380
executor.clone(),
381+
leader_gate.clone(),
374382
)
375383
.await;
376384

0 commit comments

Comments
 (0)