Skip to content

Commit 694c38b

Browse files
authored
feat: metactl: add dump-raft-log-wal subcommand (databendlabs#18865)
Add `dump-raft-log-wal` subcommand to databend-metactl for dumping raft log WAL contents in human-readable format. The command accepts a `--raft-dir` parameter pointing to the meta directory (e.g., `.databend/meta1`) and automatically appends the `/df_meta/V004/log` path suffix to locate the WAL files. The output uses Display format to provide readable representation of raft log records including state changes, vote operations, log entries, and commit markers. Each record shows byte offsets, sizes, and formatted content with log IDs in compact notation (e.g., `T1-N1.2` for term 1, node 1, index 2).
1 parent 9f8f68c commit 694c38b

File tree

7 files changed

+154
-1
lines changed

7 files changed

+154
-1
lines changed

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.

src/meta/binaries/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ futures = { workspace = true }
5050
log = { workspace = true }
5151
mlua = { workspace = true }
5252
num_cpus = { workspace = true }
53+
raft-log = { workspace = true }
5354
rand = { workspace = true }
5455
serde = { workspace = true }
5556
serde_json = { workspace = true }

src/meta/binaries/metactl/main.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use databend_common_meta_client::ClientHandle;
2828
use databend_common_meta_client::MetaGrpcClient;
2929
use databend_common_meta_control::admin::MetaAdminClient;
3030
use databend_common_meta_control::args::BenchArgs;
31+
use databend_common_meta_control::args::DumpRaftLogWalArgs;
3132
use databend_common_meta_control::args::ExportArgs;
3233
use databend_common_meta_control::args::GetArgs;
3334
use databend_common_meta_control::args::GlobalArgs;
@@ -309,6 +310,30 @@ return metrics, nil
309310
fn new_grpc_client(&self, addresses: Vec<String>) -> Result<Arc<ClientHandle>, CreationError> {
310311
lua_support::new_grpc_client(addresses, &BUILD_INFO)
311312
}
313+
314+
async fn dump_raft_log_wal(&self, args: &DumpRaftLogWalArgs) -> anyhow::Result<()> {
315+
use std::path::PathBuf;
316+
317+
use raft_log::Config;
318+
use raft_log::DumpApi;
319+
320+
let mut wal_dir = PathBuf::from(&args.raft_dir);
321+
wal_dir.push("df_meta");
322+
wal_dir.push("V004");
323+
wal_dir.push("log");
324+
325+
let config = Arc::new(Config {
326+
dir: wal_dir.to_string_lossy().to_string(),
327+
..Default::default()
328+
});
329+
330+
let dump =
331+
raft_log::Dump::<databend_common_meta_raft_store::raft_log_v004::RaftLogTypes>::new(
332+
config,
333+
)?;
334+
dump.write_display(io::stdout())?;
335+
Ok(())
336+
}
312337
}
313338

314339
#[derive(Debug, Clone, Deserialize, Subcommand)]
@@ -328,6 +353,7 @@ enum CtlCommand {
328353
Lua(LuaArgs),
329354
MemberList(MemberListArgs),
330355
Metrics(MetricsArgs),
356+
DumpRaftLogWal(DumpRaftLogWalArgs),
331357
}
332358

333359
/// Usage:
@@ -411,6 +437,9 @@ async fn main() -> anyhow::Result<()> {
411437
CtlCommand::Metrics(args) => {
412438
app.get_metrics(args).await?;
413439
}
440+
CtlCommand::DumpRaftLogWal(args) => {
441+
app.dump_raft_log_wal(args).await?;
442+
}
414443
},
415444
// for backward compatibility
416445
None => {

src/meta/control/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,10 @@ pub struct KeysLayoutArgs {
295295
#[clap(long)]
296296
pub depth: Option<u32>,
297297
}
298+
299+
#[derive(Debug, Clone, Deserialize, Args)]
300+
pub struct DumpRaftLogWalArgs {
301+
/// The dir to store persisted meta state, e.g., `.databend/meta1`
302+
#[clap(long)]
303+
pub raft_dir: String,
304+
}

src/meta/raft-store/src/raft_log_v004/log_store_meta.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use std::fmt;
1516
use std::io;
1617

1718
use databend_common_meta_types::raft_types;
@@ -25,6 +26,12 @@ pub struct LogStoreMeta {
2526
pub node_id: Option<raft_types::NodeId>,
2627
}
2728

29+
impl fmt::Display for LogStoreMeta {
30+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31+
write!(f, "LogStoreMeta{{ node_id: {:?} }}", self.node_id)
32+
}
33+
}
34+
2835
impl raft_log::codeq::Encode for LogStoreMeta {
2936
fn encode<W: io::Write>(&self, mut w: W) -> Result<usize, io::Error> {
3037
let mut ow = OffsetWriter::new(&mut w);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python3
2+
3+
import re
4+
import shutil
5+
import time
6+
from metactl_utils import metactl_bin, metactl_upsert, metactl_trigger_snapshot
7+
from utils import run_command, kill_databend_meta, start_meta_node, print_title
8+
9+
10+
def test_dump_raft_log_wal():
11+
"""Test dump-raft-log-wal subcommand from raft directory."""
12+
print_title("Test dump-raft-log-wal from raft directory")
13+
kill_databend_meta()
14+
shutil.rmtree(".databend", ignore_errors=True)
15+
start_meta_node(1, False)
16+
17+
grpc_addr = "127.0.0.1:9191"
18+
admin_addr = "127.0.0.1:28101"
19+
20+
# Insert test data
21+
test_keys = [
22+
("app/db/host", "localhost"),
23+
("app/db/port", "5432"),
24+
("app/config/timeout", "30"),
25+
]
26+
27+
for key, value in test_keys:
28+
metactl_upsert(grpc_addr, key, value)
29+
print("✓ Test data inserted")
30+
31+
# Trigger snapshot to ensure raft log has data
32+
metactl_trigger_snapshot(admin_addr)
33+
print("✓ Snapshot triggered")
34+
35+
# Wait for snapshot to complete
36+
time.sleep(2)
37+
38+
# Stop meta service before accessing raft directory
39+
kill_databend_meta()
40+
41+
# Test dump-raft-log-wal from raft directory
42+
result = run_command([metactl_bin, "dump-raft-log-wal", "--raft-dir", "./.databend/meta1"])
43+
44+
print("Output:")
45+
print(result)
46+
47+
# Expected output with time field that will be masked
48+
expected = """RaftLog:
49+
ChunkId(00_000_000_000_000_000_000)
50+
R-00000: [000_000_000, 000_000_018) Size(18): RaftLogState(RaftLogState(vote: None, last: None, committed: None, purged: None, user_data: None))
51+
R-00001: [000_000_018, 000_000_046) Size(28): RaftLogState(RaftLogState(vote: None, last: None, committed: None, purged: None, user_data: LogStoreMeta{ node_id: Some(1) }))
52+
R-00002: [000_000_046, 000_000_125) Size(79): Append(log_id: T0-N0.0, payload: membership:{voters:[{1:EmptyNode}], learners:[]})
53+
R-00003: [000_000_125, 000_000_175) Size(50): SaveVote(<T1-N1:->)
54+
R-00004: [000_000_175, 000_000_225) Size(50): SaveVote(<T1-N1:Q>)
55+
R-00005: [000_000_225, 000_000_277) Size(52): Append(log_id: T1-N1.1, payload: blank)
56+
R-00006: [000_000_277, 000_000_472) Size(195): Append(log_id: T1-N1.2, payload: normal:time: <TIME> cmd: add_node(no-override):1=id=1 raft=localhost:28103 grpc=127.0.0.1:9191)
57+
R-00007: [000_000_472, 000_000_518) Size(46): Commit(T1-N1.1)
58+
R-00008: [000_000_518, 000_000_564) Size(46): Commit(T1-N1.2)
59+
R-00009: [000_000_564, 000_000_643) Size(79): Append(log_id: T1-N1.3, payload: membership:{voters:[{1:EmptyNode}], learners:[]})
60+
R-00010: [000_000_643, 000_000_689) Size(46): Commit(T1-N1.3)
61+
R-00011: [000_000_689, 000_000_884) Size(195): Append(log_id: T1-N1.4, payload: normal:time: <TIME> cmd: add_node(override):1=id=1 raft=localhost:28103 grpc=127.0.0.1:9191)
62+
R-00012: [000_000_884, 000_000_930) Size(46): Commit(T1-N1.4)
63+
R-00013: [000_000_930, 000_001_192) Size(262): Append(log_id: T1-N1.5, payload: normal:time: <TIME> cmd: txn:TxnRequest{if:[app/db/host >= seq(0)] then:[Put(Put key=app/db/host)] else:[Get(Get key=app/db/host)]})
64+
R-00014: [000_001_192, 000_001_238) Size(46): Commit(T1-N1.5)
65+
R-00015: [000_001_238, 000_001_495) Size(257): Append(log_id: T1-N1.6, payload: normal:time: <TIME> cmd: txn:TxnRequest{if:[app/db/port >= seq(0)] then:[Put(Put key=app/db/port)] else:[Get(Get key=app/db/port)]})
66+
R-00016: [000_001_495, 000_001_541) Size(46): Commit(T1-N1.6)
67+
R-00017: [000_001_541, 000_001_817) Size(276): Append(log_id: T1-N1.7, payload: normal:time: <TIME> cmd: txn:TxnRequest{if:[app/config/timeout >= seq(0)] then:[Put(Put key=app/config/timeout)] else:[Get(Get key=app/config/timeout)]})
68+
R-00018: [000_001_817, 000_001_863) Size(46): Commit(T1-N1.7)"""
69+
70+
def mask_time(text):
71+
"""Mask time fields in the output for comparison."""
72+
# Mask timestamp in format "time: 2025-10-19T15:03:55.193"
73+
text = re.sub(r'time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+', 'time: <TIME>', text)
74+
return text
75+
76+
# Normalize both actual and expected output
77+
actual_lines = result.strip().split("\n")
78+
expected_lines = expected.strip().split("\n")
79+
80+
# Mask time in actual output
81+
actual_masked = [mask_time(line) for line in actual_lines]
82+
83+
# Verify line count matches
84+
assert len(actual_masked) == len(expected_lines), (
85+
f"Line count mismatch: got {len(actual_masked)} lines, expected {len(expected_lines)} lines"
86+
)
87+
88+
# Compare line by line
89+
for i, (actual_line, expected_line) in enumerate(zip(actual_masked, expected_lines)):
90+
assert actual_line == expected_line, (
91+
f"Line {i} mismatch:\n"
92+
f" Actual : {actual_line}\n"
93+
f" Expected: {expected_line}"
94+
)
95+
96+
print(f"✓ All {len(actual_masked)} lines match expected output (time fields masked)")
97+
98+
# Clean up only on success
99+
shutil.rmtree(".databend", ignore_errors=True)
100+
101+
102+
def main():
103+
"""Main function to run all dump-raft-log-wal tests."""
104+
test_dump_raft_log_wal()
105+
106+
107+
if __name__ == "__main__":
108+
main()

tests/metactl/test_all_subcommands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python2
22
"""
33
Comprehensive test runner for all metactl subcommands.
44

0 commit comments

Comments
 (0)