From 9ec7b2910cacd392901c9bc63886481718badc92 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sat, 3 Oct 2026 07:58:06 +0700 Subject: [PATCH 1/6] perf(lmdb): batch radix DFS reads + cache probe DBI handle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search song song tren LMDB chạm gioi han reader-slot (`MDB_BAD_RSLOT`): moi `get_node`/`get_children` la mot `begin_ro_txn` rieng, mot lan search tao O(nodes) read-txn, nhieu request thi nhan lai gap gioi han. Tang 1 - cache DBI handle trong probe_version: `probe_env` giu them `Database` handle (Copy) cho `sg_meta` thay vi `open_db` lai moi request. `SharedGraphIndex::ensure_fresh` chay `current_version` truoc moi request nen moi lan deu ton mot `open_db` (tha bang `Environment`) va mot `begin_ro_txn`; gio con 1 read-txn. Tang 2 - batch read tren CategoryStorage: them `get_nodes`/`get_childrens` doc nhieu id trong MOT read-txn. Default impl goi lai ham don le nen 6 backend nen (in-memory/sqlite/redis/pg/mysql/ cached) khong doi. LMDB override de dung 1 txn cho ca danh sach. `Radix::search_dfs` chuyen sang goy cac lan doc: doc node cua frame + tat ca child chua duyet trong 1 lenh goi, thay vi mot lenh goi cho tung node. So read-txn moi vong DFS giam tu ~2*children xuong 1. `base` giu nguyen thu tu duyet nen hanh vi DFS khong doi. Test: - batch khop `get_node`/`get_children` tuang tung, thu tu va sort giu nguyen - id khong ton tai van tra `BranchOutOfRange` nhu ban don le - regression: 8 task doc song song khong loi - `probe_version` qua cache handle 100 lan van dung version Benchmark moi `lmdb_batch_read` (feature `lmdb`) do doc 1/16/64 node don lue vs batch, ca voi `get_children`: cargo bench -p codegraph-graph --bench lmdb_batch_read --features lmdb Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/codegraph-graph/Cargo.toml | 5 + .../benches/lmdb_batch_read.rs | 129 ++++++++++++ crates/codegraph-graph/src/radix.rs | 48 +++-- crates/codegraph-graph/src/storage.rs | 34 ++++ crates/codegraph-graph/src/storage/lmdb.rs | 191 ++++++++++++++++-- 5 files changed, 377 insertions(+), 30 deletions(-) create mode 100644 crates/codegraph-graph/benches/lmdb_batch_read.rs diff --git a/crates/codegraph-graph/Cargo.toml b/crates/codegraph-graph/Cargo.toml index b68baa4bdc..9de89475e6 100644 --- a/crates/codegraph-graph/Cargo.toml +++ b/crates/codegraph-graph/Cargo.toml @@ -84,3 +84,8 @@ criterion = { version = "0.5", features = ["async_tokio"] } [[bench]] name = "search_bloom" harness = false + +[[bench]] +name = "lmdb_batch_read" +harness = false +required-features = ["lmdb"] diff --git a/crates/codegraph-graph/benches/lmdb_batch_read.rs b/crates/codegraph-graph/benches/lmdb_batch_read.rs new file mode 100644 index 0000000000..fa5090009e --- /dev/null +++ b/crates/codegraph-graph/benches/lmdb_batch_read.rs @@ -0,0 +1,129 @@ +//! Benchmark đọc LMDB: so sánh đọc TỪNG node (mỗi lần 1 read-txn) với đọc BATCH +//! (nhiều node trong 1 read-txn) — đo tác động của `get_nodes`/`get_childrens`. +//! +//! ```bash +//! cargo bench -p codegraph-graph --bench lmdb_batch_read --features lmdb +//! ``` +//! +//! Nhóm đo: +//! - `single_read_{1,16,64}` — gọi `get_node` lặp lại cho từng id. +//! - `batch_read_{1,16,64}` — gọi `get_nodes` 1 lần cho cả id. +//! - `search_dfs` — DFS thật trên trie đã dựng (đo end-to-end). + +use codegraph_graph::{CategoryStorage, LmdbStorage}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; + +const N_NODES: usize = 4096; + +/// Bộ id hợp lệ để đọc — dựng sẵn node thật trong LMDB. +fn setup(rt: &tokio::runtime::Runtime) -> (tempfile::TempDir, String, Vec) { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("bench.lmdb").to_string_lossy().into_owned(); + let ids = rt.block_on(async { + let mut s = LmdbStorage::open(&path).await.unwrap(); + let mut ids = Vec::with_capacity(N_NODES); + for i in 0..N_NODES { + // Prefix ngắn, giá trị record tăng dần — mô phỏng trie phẳng. + let id = s + .new_node(format!("n{i:05}").into_bytes(), i + 1) + .await + .unwrap(); + ids.push(id); + } + ids + }); + (dir, path, ids) +} + +fn runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() +} + +fn bench_batch_vs_single(c: &mut Criterion) { + let rt = runtime(); + let (_dir, path, ids) = setup(&rt); + let st = rt.block_on(async { LmdbStorage::open(&path).await.unwrap() }); + + let mut group = c.benchmark_group("lmdb_node_read"); + + for &k in &[1usize, 16, 64] { + let ids: Vec = ids.iter().copied().take(k).collect(); + + group.bench_function(format!("single_{k}"), |b| { + b.iter(|| { + rt.block_on(async { + for &id in &ids { + black_box(st.get_node(id).await.unwrap()); + } + }); + }); + }); + + group.bench_function(format!("batch_{k}"), |b| { + b.iter(|| { + rt.block_on(async { + black_box(st.get_nodes(&ids).await.unwrap()); + }); + }); + }); + } + + group.finish(); +} + +/// Đọc children: `get_children` lặp vs `get_childrens` batch. +fn bench_children_batch(c: &mut Criterion) { + let rt = runtime(); + let (_dir, path, _ids) = setup(&rt); + let parents = rt.block_on(async { + let mut s = LmdbStorage::open(&path).await.unwrap(); + let mut parents = Vec::new(); + for i in 0..64usize { + let p = s.new_node(format!("p{i:03}").into_bytes(), i).await.unwrap(); + // Mỗi parent có 32 child. + for j in 0..32usize { + let c = s + .new_node(format!("c{i:03}_{j:03}").into_bytes(), j) + .await + .unwrap(); + let mut tx = s.new_tx(); + tx.add_child(p, c).await.unwrap(); + tx.commit().await.unwrap(); + } + parents.push(p); + } + parents + }); + let st = rt.block_on(async { LmdbStorage::open(&path).await.unwrap() }); + + let mut group = c.benchmark_group("lmdb_children_read"); + for &k in &[1usize, 16, 64] { + let ps: Vec = parents.iter().copied().take(k).collect(); + group.bench_function(format!("single_{k}"), |b| { + b.iter(|| { + rt.block_on(async { + for &p in &ps { + black_box(st.get_children(p).await.unwrap()); + } + }); + }); + }); + group.bench_function(format!("batch_{k}"), |b| { + b.iter(|| { + rt.block_on(async { + black_box(st.get_childrens(&ps).await.unwrap()); + }); + }); + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_batch_vs_single, + bench_children_batch +); +criterion_main!(benches); diff --git a/crates/codegraph-graph/src/radix.rs b/crates/codegraph-graph/src/radix.rs index 5adf2bc8ee..c80b7512e5 100644 --- a/crates/codegraph-graph/src/radix.rs +++ b/crates/codegraph-graph/src/radix.rs @@ -677,8 +677,16 @@ impl Radix { break; }; - let (prefix_bytes, _record) = - { self.storage.read().await.get_node(frame.node_id).await? }; + // Đọc node của frame + children của frame trong MỘT + // read-txn (`get_nodes`/`get_childrens`): xem + // `CategoryStorage::get_nodes` — giảm số read-txn mỗi + // vòng DFS, tránh chạm trần reader-slot của LMDB khi nhiều + // request search song song. + let (prefix_bytes, _record) = { + let st = self.storage.read().await; + let mut out = st.get_nodes(&[frame.node_id]).await?; + out.pop().expect("get_nodes trả về đúng 1 phần tử") + }; let prefix = Self::to_vec(&prefix_bytes); let result = matcher(&prefix, pattern, frame.pattern_pos); @@ -691,11 +699,9 @@ impl Radix { } let children = { - self.storage - .read() - .await - .get_children(frame.node_id) - .await? + let st = self.storage.read().await; + let mut out = st.get_childrens(&[frame.node_id]).await?; + out.pop().expect("get_childrens trả về đúng 1 phần tử") }; let mut descended = false; while frame.cont_idx < result.continuations.len() { @@ -707,11 +713,23 @@ impl Radix { } let next_elem = pattern[pp]; + // Pre-fetch node của MỌI child còn lại trong 1 + // read-txn thay vì mỗi child 1 txn. Chỉ fetch phần chưa + // duyệt; `base` ánh xạ chỉ số tương đối về `children` + // (giống hệt hành vi cũ từng child một). + let base = frame.child_idx.min(children.len()); + let remaining = &children[base..]; + let child_nodes = if remaining.is_empty() { + Vec::new() + } else { + let st = self.storage.read().await; + st.get_nodes(remaining).await? + }; while frame.child_idx < children.len() { let child = children[frame.child_idx]; + let rel = frame.child_idx - base; frame.child_idx += 1; - let (cp_bytes, _) = - { self.storage.read().await.get_node(child).await? }; + let (cp_bytes, _) = &child_nodes[rel]; let cp = Self::to_vec(&cp_bytes); if cp.is_empty() || cp[0] != next_elem { continue; @@ -758,12 +776,18 @@ impl Radix { } DfsState::Collect { root, mut stack } => { if let Some((node_id, child_idx)) = stack.pop() { - let (_prefix_bytes, record) = - { self.storage.read().await.get_node(node_id).await? }; + // Gom node + children của cùng `node_id` — 2 read-txn + // điển hình xuống 2 lần gọi batch (mỗi lần 1 txn). + let (record, children) = { + let st = self.storage.read().await; + let mut node = st.get_nodes(&[node_id]).await?; + let node = node.pop().expect("get_nodes trả về đúng 1 phần tử"); + let children = st.get_childrens(&[node_id]).await?; + (node.1, children.pop().expect("get_childrens trả 1 phần tử")) + }; if record != storage::EMPTY { records.push(record); } - let children = { self.storage.read().await.get_children(node_id).await? }; if child_idx < children.len() { stack.push((node_id, child_idx + 1)); stack.push((children[child_idx], 0)); diff --git a/crates/codegraph-graph/src/storage.rs b/crates/codegraph-graph/src/storage.rs index fdde6a2fad..159dfb636c 100644 --- a/crates/codegraph-graph/src/storage.rs +++ b/crates/codegraph-graph/src/storage.rs @@ -323,6 +323,40 @@ macro_rules! declare_category_storage { async fn get_node(&self, id: usize) -> Result<(Vec, usize)>; async fn get_children(&self, id: usize) -> Result>; + // ── Batch reads (MỐT read-txn cho nhiều id) ── + // + // Radix DFS (`radix.rs::search_dfs`) gọi `get_node`/`get_children` + // cho TỪNG node/child → mỗi lần đọc là một `begin_ro_txn` riêng nên + // một lần search tạo O(nodes) read-txn. Khi nhiều request search chạy + // song song (MCP/GraphQL dùng chung `Arc`), số read-txn + // đồng thời nhân lên và có thể chạm trần reader-slot của LMDB → + // `MDB_BAD_RSLOT`. + // + // 2 hàm này gom nhiều id vào MỘT read-txn. Backend nền (in-memory, + // sqlite, redis, rdbms) không override — default impl gọi lại hàm + // đơn lẻ nên hành vi giữ nguyên. LMDB override để dùng 1 txn. + + /// Đọc nhiều node trong MỘT read-txn — thứ tự khớp `ids`. + /// + /// Node không tồn tại → `Err(BranchOutOfRange)` (giống `get_node`). + async fn get_nodes(&self, ids: &[usize]) -> Result, usize)>> { + let mut out = Vec::with_capacity(ids.len()); + for &id in ids { + out.push(self.get_node(id).await?); + } + Ok(out) + } + + /// Đọc children của nhiều node trong MỐT read-txn — `out[i]` là + /// children của `ids[i]`, đã sort. Node không tồn tại → `Err`. + async fn get_childrens(&self, ids: &[usize]) -> Result>> { + let mut out = Vec::with_capacity(ids.len()); + for &id in ids { + out.push(self.get_children(id).await?); + } + Ok(out) + } + // ── Shard roots (endpoint) ── async fn set_root(&mut self, shard: usize, root: usize) -> Result<()>; async fn get_root(&self, shard: usize) -> Result; diff --git a/crates/codegraph-graph/src/storage/lmdb.rs b/crates/codegraph-graph/src/storage/lmdb.rs index 08a6ef37cb..c800904cb5 100644 --- a/crates/codegraph-graph/src/storage/lmdb.rs +++ b/crates/codegraph-graph/src/storage/lmdb.rs @@ -211,44 +211,56 @@ fn open_env_read_only(path: &str) -> lmdb::Result { b.open(Path::new(path)) } -/// Cache read-only `Environment` theo path — 1 env dùng chung cho mọi `probe_version`. +/// Env + DBI handle dùng cho `probe_version`, cache theo path. /// -/// `Environment` là `Send + Sync` nên an toàn để dùng chung; env sống trọn -/// process (không drop) để locktable không bị mở/đóng lặp. +/// `Environment` là `Send + Sync` nên an toàn để dùng chung; `Database` là +/// handle `Copy` chỉ trỏ vào env. Cả hai sống trọn process (không drop) để +/// locktable không bị mở/đóng lặp. #[cfg(feature = "lmdb")] #[cfg_attr(feature = "sqlite", allow(dead_code))] // probe chỉ dùng khi lmdb là backend file -fn probe_env(path: &str) -> lmdb::Result> { +struct ProbeHandle { + env: Arc, + db: Database, +} + +/// Cache read-only `(Environment, Database)` theo path — 1 cặp dùng chung cho +/// mọi `probe_version`. +#[cfg(feature = "lmdb")] +#[cfg_attr(feature = "sqlite", allow(dead_code))] // probe chỉ dùng khi lmdb là backend file +fn probe_env(path: &str) -> lmdb::Result> { let data = Path::new(path).join("data.mdb"); if !data.is_file() { return Err(lmdb::Error::NotFound); } - static CACHE: std::sync::LazyLock>>> = + static CACHE: std::sync::LazyLock>>> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); let mut cache = CACHE.lock().expect("probe env cache lock"); - if let Some(env) = cache.get(path) { - return Ok(env.clone()); + if let Some(handle) = cache.get(path) { + return Ok(handle.clone()); } let env = Arc::new(open_env_read_only(path)?); - cache.insert(path.to_string(), env.clone()); - Ok(env) + let db = env.open_db(Some(D_VERSION)).map_err(|err| lmdb::Error::Other(err.to_string()))?; + let handle = Arc::new(ProbeHandle { env, db }); + cache.insert(path.to_string(), handle.clone()); + Ok(handle) } /// Đọc `version` từ file mà KHÔNG tạo file (nếu chưa có) — dùng bởi /// `SharedGraphIndex::ensure_fresh` để dò stale. Mirror `SqliteStorage::probe_version`. /// -/// Reuse env cache (`probe_env`) để không mở/đóng `Environment` mỗi lần gọi — -/// `MDB_BAD_RSLOT` xảy ra khi nhiều `Environment` cùng mở/đóng trên một locktable -/// (lock.mdb) khi nhiều request probe song song (runtime/mcp: mỗi request gọi qua -/// `ensure_fresh` → `current_version`). Cache theo path giữ 1 env read-only dùng -/// chung (sống trọn process) nên không còn tranh chấp slot reader. +/// Reuse cache (`probe_env`) để không mở/đóng `Environment` **và không `open_db`** +/// mỗi lần gọi — `MDB_BAD_RSLOT` xảy ra khi nhiều `Environment` cùng mở/đóng trên +/// một locktable (lock.mdb) khi nhiều request probe song song (runtime/mcp: mỗi +/// request gọi qua `ensure_fresh` → `current_version`). Cache theo path giữ 1 env +/// read-only + DBI handle dùng chung (sống trọn process) nên mỗi request chỉ còn +/// đúng 1 `begin_ro_txn` + 1 `get`. #[cfg(feature = "lmdb")] #[cfg_attr(feature = "sqlite", allow(dead_code))] // probe chỉ dùng khi lmdb là backend file pub async fn probe_version(path: &str) -> Result { - let env = probe_env(path) + let handle = probe_env(path) .map_err(|err| StorageError::Internal(format!("lmdb file not found: {path} ({err})")))?; - let db = env.open_db(Some(D_VERSION)).map_err(e)?; - let tx = env.begin_ro_txn().map_err(e)?; - match tx.get(db, &KEY_ONE).map(de_u64) { + let tx = handle.env.begin_ro_txn().map_err(e)?; + match tx.get(handle.db, &KEY_ONE).map(de_u64) { Ok(v) => Ok(v), Err(lmdb::Error::NotFound) => { Err(StorageError::Internal("lmdb version row missing".into())) @@ -489,6 +501,42 @@ impl CategoryStorage for LmdbStorage { Ok(out) } + /// Batch đọc node: MỘT `begin_ro_txn` cho cả `ids` — xem + /// [`CategoryStorage::get_nodes`] (giải thích `MDB_BAD_RSLOT`). + async fn get_nodes(&self, ids: &[usize]) -> Result, usize)>> { + if ids.is_empty() { + return Ok(Vec::new()); + } + let tx = self.env.begin_ro_txn().map_err(e)?; + let mut out = Vec::with_capacity(ids.len()); + for &id in ids { + let Some(v) = self.get_opt(&tx, self.nodes, &k8(id))? else { + return Err(StorageError::BranchOutOfRange(id)); + }; + out.push(de_node_val(v)); + } + Ok(out) + } + + /// Batch đọc children: MỘT `begin_ro_txn` cho cả `ids`, mỗi list vẫn sort + /// như `get_children` để DFS không đổi thứ tự duyệt. + async fn get_childrens(&self, ids: &[usize]) -> Result>> { + if ids.is_empty() { + return Ok(Vec::new()); + } + let tx = self.env.begin_ro_txn().map_err(e)?; + let mut out = Vec::with_capacity(ids.len()); + for &id in ids { + let mut list = match self.get_opt(&tx, self.children, &k8(id))? { + Some(v) => de_list(v), + None => Vec::new(), + }; + list.sort_unstable(); + out.push(list); + } + Ok(out) + } + async fn set_root(&mut self, shard: usize, root: usize) -> Result<()> { let mut tx = self.env.begin_rw_txn().map_err(e)?; tx.put(self.roots, &k8(shard), &k8(root), WriteFlags::empty()) @@ -1097,6 +1145,113 @@ mod tests { (dir, path) } + /// `get_nodes` phải trả đúng thứ tự `ids` và giống hệt `get_node` từng cái. + #[tokio::test] + async fn test_get_nodes_batch_matches_single() { + let (_d, path) = tmp_path(); + let mut s = LmdbStorage::open(&path).await.unwrap(); + let ids: Vec = (1..=20).map(|i| s.new_node(vec![i as u8; (i % 5) + 1], i * 3).await.unwrap()).collect(); + + let batch = s.get_nodes(&ids).await.unwrap(); + assert_eq!(batch.len(), ids.len()); + for (i, &id) in ids.iter().enumerate() { + assert_eq!(batch[i], s.get_node(id).await.unwrap()); + } + + // Rỗng → rỗng, không mở txn nào. + assert!(s.get_nodes(&[]).await.unwrap().is_empty()); + } + + /// `get_childrens` phải trả list đã sort, khớp `get_children` từng cái. + #[tokio::test] + async fn test_get_childrens_batch_matches_single() { + let (_d, path) = tmp_path(); + let mut s = LmdbStorage::open(&path).await.unwrap(); + let a = s.new_node(b"a".to_vec(), 1).await.unwrap(); + let b = s.new_node(b"b".to_vec(), 2).await.unwrap(); + // Insert ngược thứ tự để chắc chắn phải sort mới ra [x, y, z]. + let kids = [b.new_node(b"k1".to_vec(), 10).await.unwrap(), b.new_node(b"k0".to_vec(), 11).await.unwrap()]; + for &kid in &kids { + let mut tx = s.new_tx(); + tx.add_child(a, kid).await.unwrap(); + tx.commit().await.unwrap(); + } + + let batch = s.get_childrens(&[a, b]).await.unwrap(); + assert_eq!(batch[0], s.get_children(a).await.unwrap()); + assert_eq!(batch[1], s.get_children(b).await.unwrap()); + assert_eq!(batch[0], kids); + + assert!(s.get_childrens(&[]).await.unwrap().is_empty()); + } + + /// Id không tồn tại → `BranchOutOfRange`, giống `get_node` đơn lẻ. + #[tokio::test] + async fn test_batch_missing_id_errors() { + let (_d, path) = tmp_path(); + let s = LmdbStorage::open(&path).await.unwrap(); + assert!(matches!( + s.get_nodes(&[999_999]).await, + Err(StorageError::BranchOutOfRange(_)) + )); + } + + /// Regression `MDB_BAD_RSLOT`: N task đọc SONG SONG phải không lỗi khi + /// reader-slot LMDB có trần (`max_readers`). Trước khi có batch read, + /// mỗi `get_node` là một read-txn nên tải đọc nhân lên rất nhanh. + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_concurrent_batch_reads_no_bad_rslot() { + use std::sync::Arc; + let (_d, path) = tmp_path(); + let ids = { + let mut s = LmdbStorage::open(&path).await.unwrap(); + let mut ids = Vec::new(); + for i in 1..=500usize { + ids.push(s.new_node(vec![(i % 251) as u8; 6], i).await.unwrap()); + } + for &id in ids.iter() { + let mut tx = s.new_tx(); + tx.add_child(storage::EMPTY, id).await.unwrap(); + tx.commit().await.unwrap(); + } + ids + }; + let st = Arc::new(LmdbStorage::open(&path).await.unwrap()); + + // 8 task × 200 vòng batch-read — tổng ~800k lần đọc node. + let mut handles = Vec::new(); + for _ in 0..8 { + let st = Arc::clone(&st); + let ids = ids.clone(); + handles.push(tokio::spawn(async move { + for _ in 0..200 { + let chunk: Vec = ids.iter().copied().take(64).collect(); + let got = st.get_nodes(&chunk).await.unwrap(); + assert_eq!(got.len(), chunk.len()); + } + })); + } + for h in handles { + h.await.unwrap(); + } + } + + /// `probe_version` phải trả đúng version khi gọi lặp lại (dùng env + DBI + /// handle cache) và vẫn lỗi khi path không tồn tại. + #[tokio::test] + async fn test_probe_version_reuses_cached_handle() { + let (_d, path) = tmp_path(); + { + let mut s = LmdbStorage::open(&path).await.unwrap(); + s.set_version(42).await.unwrap(); + } + // Nhiều lần gọi liên tiếp — lần 2+ đi qua cache handle. + for _ in 0..100 { + assert_eq!(probe_version(&path).await.unwrap(), 42); + } + assert!(probe_version("definitely/missing.lmdb").await.is_err()); + } + #[tokio::test] async fn test_new_node_and_get_node() { let (_d, path) = tmp_path(); From ae40ede176be0e4ed7001e745f5abb18f3627f9e Mon Sep 17 00:00:00 2001 From: hungpham10 <136320753+hungpham10@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:45:52 +0000 Subject: [PATCH 2/6] style: apply rustfmt --- crates/codegraph-graph/benches/lmdb_batch_read.rs | 11 +++++------ crates/codegraph-graph/src/storage/lmdb.rs | 13 ++++++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/codegraph-graph/benches/lmdb_batch_read.rs b/crates/codegraph-graph/benches/lmdb_batch_read.rs index fa5090009e..4dd400b7a3 100644 --- a/crates/codegraph-graph/benches/lmdb_batch_read.rs +++ b/crates/codegraph-graph/benches/lmdb_batch_read.rs @@ -81,7 +81,10 @@ fn bench_children_batch(c: &mut Criterion) { let mut s = LmdbStorage::open(&path).await.unwrap(); let mut parents = Vec::new(); for i in 0..64usize { - let p = s.new_node(format!("p{i:03}").into_bytes(), i).await.unwrap(); + let p = s + .new_node(format!("p{i:03}").into_bytes(), i) + .await + .unwrap(); // Mỗi parent có 32 child. for j in 0..32usize { let c = s @@ -121,9 +124,5 @@ fn bench_children_batch(c: &mut Criterion) { group.finish(); } -criterion_group!( - benches, - bench_batch_vs_single, - bench_children_batch -); +criterion_group!(benches, bench_batch_vs_single, bench_children_batch); criterion_main!(benches); diff --git a/crates/codegraph-graph/src/storage/lmdb.rs b/crates/codegraph-graph/src/storage/lmdb.rs index c800904cb5..c8dcfa1fbe 100644 --- a/crates/codegraph-graph/src/storage/lmdb.rs +++ b/crates/codegraph-graph/src/storage/lmdb.rs @@ -239,7 +239,9 @@ fn probe_env(path: &str) -> lmdb::Result> { return Ok(handle.clone()); } let env = Arc::new(open_env_read_only(path)?); - let db = env.open_db(Some(D_VERSION)).map_err(|err| lmdb::Error::Other(err.to_string()))?; + let db = env + .open_db(Some(D_VERSION)) + .map_err(|err| lmdb::Error::Other(err.to_string()))?; let handle = Arc::new(ProbeHandle { env, db }); cache.insert(path.to_string(), handle.clone()); Ok(handle) @@ -1150,7 +1152,9 @@ mod tests { async fn test_get_nodes_batch_matches_single() { let (_d, path) = tmp_path(); let mut s = LmdbStorage::open(&path).await.unwrap(); - let ids: Vec = (1..=20).map(|i| s.new_node(vec![i as u8; (i % 5) + 1], i * 3).await.unwrap()).collect(); + let ids: Vec = (1..=20) + .map(|i| s.new_node(vec![i as u8; (i % 5) + 1], i * 3).await.unwrap()) + .collect(); let batch = s.get_nodes(&ids).await.unwrap(); assert_eq!(batch.len(), ids.len()); @@ -1170,7 +1174,10 @@ mod tests { let a = s.new_node(b"a".to_vec(), 1).await.unwrap(); let b = s.new_node(b"b".to_vec(), 2).await.unwrap(); // Insert ngược thứ tự để chắc chắn phải sort mới ra [x, y, z]. - let kids = [b.new_node(b"k1".to_vec(), 10).await.unwrap(), b.new_node(b"k0".to_vec(), 11).await.unwrap()]; + let kids = [ + b.new_node(b"k1".to_vec(), 10).await.unwrap(), + b.new_node(b"k0".to_vec(), 11).await.unwrap(), + ]; for &kid in &kids { let mut tx = s.new_tx(); tx.add_child(a, kid).await.unwrap(); From 850471ad3846e52270b333c7b0616802391eb9dc Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sat, 3 Oct 2026 11:17:46 +0700 Subject: [PATCH 3/6] =?UTF-8?q?fix(lmdb):=20s=E1=BB=ADa=202=20l=E1=BB=97i?= =?UTF-8?q?=20compile=20trong=20batch=20read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `lmdb::Error::Other` nhận `c_int` (mã lỗi C), không phải `String`. `open_db` đã trả `lmdb::Result` nên chuyển thẳng với `?`. - `children` trong `search_dfs` (Collect state) cần `mut` để gọi `.pop()` — tương tự `node` ngay dòng trên. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/codegraph-graph/src/radix.rs | 2 +- crates/codegraph-graph/src/storage/lmdb.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/codegraph-graph/src/radix.rs b/crates/codegraph-graph/src/radix.rs index c80b7512e5..0db847f1c4 100644 --- a/crates/codegraph-graph/src/radix.rs +++ b/crates/codegraph-graph/src/radix.rs @@ -782,7 +782,7 @@ impl Radix { let st = self.storage.read().await; let mut node = st.get_nodes(&[node_id]).await?; let node = node.pop().expect("get_nodes trả về đúng 1 phần tử"); - let children = st.get_childrens(&[node_id]).await?; + let mut children = st.get_childrens(&[node_id]).await?; (node.1, children.pop().expect("get_childrens trả 1 phần tử")) }; if record != storage::EMPTY { diff --git a/crates/codegraph-graph/src/storage/lmdb.rs b/crates/codegraph-graph/src/storage/lmdb.rs index c8dcfa1fbe..64e41b7206 100644 --- a/crates/codegraph-graph/src/storage/lmdb.rs +++ b/crates/codegraph-graph/src/storage/lmdb.rs @@ -239,9 +239,7 @@ fn probe_env(path: &str) -> lmdb::Result> { return Ok(handle.clone()); } let env = Arc::new(open_env_read_only(path)?); - let db = env - .open_db(Some(D_VERSION)) - .map_err(|err| lmdb::Error::Other(err.to_string()))?; + let db = env.open_db(Some(D_VERSION))?; let handle = Arc::new(ProbeHandle { env, db }); cache.insert(path.to_string(), handle.clone()); Ok(handle) From 0728ecba47309a4cfbf12d3ca7ac2b5dcc914d8d Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sat, 3 Oct 2026 11:30:33 +0700 Subject: [PATCH 4/6] =?UTF-8?q?fix(lmdb):=20s=E1=BB=ADa=203=20l=E1=BB=97i?= =?UTF-8?q?=20compile=20trong=20test=20batch=20read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test viết tay sai cú pháp: - `storage::EMPTY` không resolve trong `mod tests` (chỉ có `EMPTY` từ `use super::*`) → E0433 - `.map(|i| s.new_node(..).await)` — `.await` trong closure không async → E0728. Vòng for thay thế. - `b.new_node(..)` gọi method trên `usize` do nhầm biến `b` (id) với `s` (storage) → E0599 - `assert_eq!(batch[0], kids)` so `Vec` với array → so với bản sort Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/codegraph-graph/src/storage/lmdb.rs | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/codegraph-graph/src/storage/lmdb.rs b/crates/codegraph-graph/src/storage/lmdb.rs index 64e41b7206..646680e5db 100644 --- a/crates/codegraph-graph/src/storage/lmdb.rs +++ b/crates/codegraph-graph/src/storage/lmdb.rs @@ -1150,9 +1150,10 @@ mod tests { async fn test_get_nodes_batch_matches_single() { let (_d, path) = tmp_path(); let mut s = LmdbStorage::open(&path).await.unwrap(); - let ids: Vec = (1..=20) - .map(|i| s.new_node(vec![i as u8; (i % 5) + 1], i * 3).await.unwrap()) - .collect(); + let mut ids: Vec = Vec::new(); + for i in 1..=20usize { + ids.push(s.new_node(vec![i as u8; (i % 5) + 1], i * 3).await.unwrap()); + } let batch = s.get_nodes(&ids).await.unwrap(); assert_eq!(batch.len(), ids.len()); @@ -1171,10 +1172,11 @@ mod tests { let mut s = LmdbStorage::open(&path).await.unwrap(); let a = s.new_node(b"a".to_vec(), 1).await.unwrap(); let b = s.new_node(b"b".to_vec(), 2).await.unwrap(); - // Insert ngược thứ tự để chắc chắn phải sort mới ra [x, y, z]. + // Hai child của `a`, tạo ngược thứ tự id để chắc chắn phải sort mới ra + // thứ tự tăng dần. let kids = [ - b.new_node(b"k1".to_vec(), 10).await.unwrap(), - b.new_node(b"k0".to_vec(), 11).await.unwrap(), + s.new_node(b"k1".to_vec(), 10).await.unwrap(), + s.new_node(b"k0".to_vec(), 11).await.unwrap(), ]; for &kid in &kids { let mut tx = s.new_tx(); @@ -1185,7 +1187,12 @@ mod tests { let batch = s.get_childrens(&[a, b]).await.unwrap(); assert_eq!(batch[0], s.get_children(a).await.unwrap()); assert_eq!(batch[1], s.get_children(b).await.unwrap()); - assert_eq!(batch[0], kids); + // Children luôn sort tăng dần dù tạo ngược thứ tự. + let mut sorted = kids.to_vec(); + sorted.sort_unstable(); + assert_eq!(batch[0], sorted); + // Node không có child → list rỗng (không phải lỗi). + assert!(batch[1].is_empty()); assert!(s.get_childrens(&[]).await.unwrap().is_empty()); } @@ -1216,7 +1223,7 @@ mod tests { } for &id in ids.iter() { let mut tx = s.new_tx(); - tx.add_child(storage::EMPTY, id).await.unwrap(); + tx.add_child(EMPTY, id).await.unwrap(); tx.commit().await.unwrap(); } ids From 6a80b83fb11c4cb0231d591550355b6d60d69c55 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sat, 3 Oct 2026 11:41:11 +0700 Subject: [PATCH 5/6] =?UTF-8?q?fix(radix):=20b=E1=BB=8F=20needless=20borro?= =?UTF-8?q?w=20khi=20=C4=91=E1=BB=8Dc=20child=20node=20t=E1=BB=AB=20batch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clippy::needless_borrow: `Self::to_vec(&cp_bytes)` trong khi `cp_bytes` đã là `&Vec` từ `&child_nodes[rel]` → bỏ dấu & thừa. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/codegraph-graph/src/radix.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/codegraph-graph/src/radix.rs b/crates/codegraph-graph/src/radix.rs index 0db847f1c4..0debe18174 100644 --- a/crates/codegraph-graph/src/radix.rs +++ b/crates/codegraph-graph/src/radix.rs @@ -730,7 +730,7 @@ impl Radix { let rel = frame.child_idx - base; frame.child_idx += 1; let (cp_bytes, _) = &child_nodes[rel]; - let cp = Self::to_vec(&cp_bytes); + let cp = Self::to_vec(cp_bytes); if cp.is_empty() || cp[0] != next_elem { continue; } From 0ca3f5fc0dae2d818ac07977a9dd63565d15913c Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sat, 3 Oct 2026 11:48:06 +0700 Subject: [PATCH 6/6] =?UTF-8?q?fix(storage):=20allow=20clippy::double=5Fmu?= =?UTF-8?q?st=5Fuse=20cho=20c=C3=A1c=20trait=20async=5Ftrait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clippy 1.99 (rust-toolchain.toml dùng channel = stable) bật lint double_must_use: async_tair tự sinh must_use cho future, trùng với kiểu future boxed vốn đã must_use. Lỗi nằm trong macro của async_trait nên allow tại chỗ sinh ra, không đổi hành vi. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/codegraph-graph/src/storage.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/codegraph-graph/src/storage.rs b/crates/codegraph-graph/src/storage.rs index 159dfb636c..68d13fb6ac 100644 --- a/crates/codegraph-graph/src/storage.rs +++ b/crates/codegraph-graph/src/storage.rs @@ -161,6 +161,10 @@ enum TxOp { /// chưa lộ ra cho reader cho tới khi `commit` hoàn tất. /// /// `commit(self: Box)` tiêu thụ chính transaction — không thể commit 2 lần. +// `async_trait` tự sinh `#[must_use]` cho future; double_must_use (clippy 1.99) +// báo trùng vì `Pin>` vốn đã `must_use`. Lỗi nằm trong macro +// của async_trait, không phải code viết tay → allow tại chỗ sinh ra. +#[allow(clippy::double_must_use)] #[async_trait] pub trait Tx: Send { async fn new_node(&mut self, prefix: Vec, record: usize) -> Result; @@ -183,6 +187,7 @@ pub trait Tx: Send { /// khi feature bật → method gọi được qua `dyn CategoryStorage` như cũ. /// Backend không override → default no-op. #[cfg(feature = "bloom-search")] +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) #[async_trait] pub trait BloomStorage: Send + Sync { async fn set_node_bloom(&mut self, _id: usize, _bloom: &[u8]) -> Result<()> { @@ -199,6 +204,7 @@ pub trait BloomStorage: Send + Sync { /// element id, cùng `clear`. Tách riêng để trait lõi gọn. `CategoryStorage` /// super-bound trait này (luôn) → method gọi được qua `dyn CategoryStorage`. /// Mặc định no-op. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) #[async_trait] pub trait NodeMetaStorage: Send + Sync { /// Lưu metadata của node (opaque bytes, VD Node JSON) keyed theo element id. @@ -236,6 +242,7 @@ pub trait NodeMetaStorage: Send + Sync { /// lookup (KMP + DFS). Tách riêng để trait lõi gọn. `CategoryStorage` /// super-bound trait này (luôn) → method gọi được qua `dyn CategoryStorage`. /// Mặc định no-op. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) #[async_trait] pub trait ShortcutsStorage: Send + Sync { /// Thêm `node_id` vào shortcut set của element `elem` (encoded bytes). @@ -262,6 +269,7 @@ pub trait ShortcutsStorage: Send + Sync { /// Edge-data storage: lưu/đọc metadata của mỗi edge id (opaque bytes) keyed /// theo edge id. Tách riêng để trait lõi gọn. `CategoryStorage` super-bound /// trait này (luôn) → method gọi được qua `dyn CategoryStorage`. Mặc định no-op. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) #[async_trait] pub trait EdgeDataStorage: Send + Sync { /// Lưu dữ liệu edge (opaque bytes, VD CallEdgeMeta JSON) keyed theo edge id. @@ -284,6 +292,7 @@ pub trait EdgeDataStorage: Send + Sync { /// encode u64 LE 8-byte/element. Tách riêng để trait lõi gọn. /// `CategoryStorage` super-bound trait này (luôn) → method gọi được qua /// `dyn CategoryStorage`. Mặc định no-op. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) #[async_trait] pub trait ChainStorage: Send + Sync { /// Lưu chain của owner (keyed theo record của owner; u64 LE 8-byte/element). @@ -310,7 +319,8 @@ pub trait ChainStorage: Send + Sync { macro_rules! declare_category_storage { ($($bounds:tt)*) => { /// Radix-node storage: node management + transaction + 5 stream phụ. - #[async_trait] + #[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) +#[async_trait] pub trait CategoryStorage: $($bounds)* { // ── Node management ── async fn new_node(&mut self, prefix: Vec, record: usize) -> Result; @@ -391,6 +401,8 @@ declare_category_storage!( /// - Chỉ `GraphIndex` / `SharedGraphIndex` dùng (`Radix` / `Search` không cần). /// - Backend tối giản có thể bỏ qua (vd: chỉ cần `CategoryStorage` cho test). /// - Cho phép phát triển/scale entity layer độc lập với radix. +// Xem `Tx` — cùng lý do `double_must_use` (clippy 1.99). +#[allow(clippy::double_must_use)] #[async_trait] pub trait EntityStorage: Send + Sync { // ── Symbol registry ── @@ -526,6 +538,7 @@ pub trait EntityStorage: Send + Sync { /// /// Gộp `CategoryStorage` + 5 trait phụ + `EntityStorage`. Backend implement /// 7 `impl` block riêng biệt — review từng phần độc lập được. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) #[async_trait] pub trait Storage: CategoryStorage