Skip to content

Conversation

@Tsukikage7
Copy link
Contributor

@Tsukikage7 Tsukikage7 commented Jan 9, 2026

关联 Issue

Closes #222

背景

当前存储层直接使用 RocksDB 的 WriteBatch 进行批量写操作,这种实现方式与底层存储引擎强耦合,无法支持后续的集群模式(需要通过 Raft 共识进行写入)。

解决方案

引入 Batch trait 抽象层,将批量写操作从具体实现中解耦:

  • Batch trait: 定义统一的批量写接口
  • RocksBatch: 独立模式实现,封装 RocksDB WriteBatch
  • BinlogBatch: 集群模式预留实现,后续用于 Raft 共识写入

主要改动

  1. 新增 batch.rs 模块,包含 Batch trait 及两种实现
  2. 在 Redis 结构体中添加 create_batch() 工厂方法
  3. 重构以下模块中的 WriteBatch 调用:
    • redis_strings.rs (2 处)
    • redis_hashes.rs (6 处)
    • redis_lists.rs (7 处)
    • redis_sets.rs (6 处)
    • redis_zsets.rs (8 处)
  4. 添加无效 ColumnFamily 索引的显式错误处理

测试情况

  • 单元测试:全部通过
  • 集成测试:全部通过(共 335 个测试用例)

Checklist

  • 代码符合项目编码规范
  • 已添加必要的注释和文档
  • 所有测试通过
  • 无新增编译警告

Summary by CodeRabbit

  • New Features

    • Added a batch abstraction for atomic multi-key/multi-structure commits and exposed a public batch creation API.
  • Refactor

    • Switched hashes, lists, sets, strings, and sorted sets to the unified batch API, consolidating write paths and ensuring single-commit atomicity.
  • Bug Fixes

    • LPUSHX/RPUSHX now leave non-existent lists unchanged (return 0).
  • Tests

    • Added/updated tests covering batch behavior and list semantics.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

📝 Walkthrough

Walkthrough

Adds a public Batch trait with two implementations (RocksBatch for standalone, BinlogBatch placeholder for cluster), re-exports them, and migrates Redis storage modules to create and use batches via Redis::create_batch(), replacing direct RocksDB WriteBatch usage.

Changes

Cohort / File(s) Summary
Batch core
src/storage/src/batch.rs, src/storage/src/lib.rs, src/storage/src/redis.rs
New Batch trait, RocksBatch (implements put/delete/commit/count/clear), BinlogBatch placeholder, CfHandles alias, EXPECTED_CF_COUNT, CF-handle validation and error mapping; mod batch added and pub use batch::{Batch, BinlogBatch, RocksBatch}; Redis::create_batch() returns Box<dyn Batch>.
Hashes
src/storage/src/redis_hashes.rs
Replaced direct WriteBatch ops with create_batch(); accumulate per-CF puts/deletes, then batch.commit() once; refactored helpers and error propagation to use batch.
Lists
src/storage/src/redis_lists.rs
Switched list mutations to use create_batch() and collected puts/deletes applied via batch.commit(); metadata and data writes batched atomically; removed direct WriteBatch usage.
Sets
src/storage/src/redis_sets.rs
Set operations (sadd/srem/spop/smove/... ) refactored to create a batch, collect deletes/writes, and commit atomically; added helpers to clear+store destination in one batch.
Strings
src/storage/src/redis_strings.rs
Many string write paths (set, mset, incr, bit ops, deletes, etc.) converted to use create_batch() and batch.commit() instead of direct db.write_opt.
Zsets
src/storage/src/redis_zsets.rs
Zset mutations and store operations updated to use create_batch() and per-CF batch.put/batch.delete across ZsetsDataCF, ZsetsScoreCF, and MetaCF, with single-commit flows.
Tests / Behavior
src/storage/tests/redis_list_test.rs
Updated LPUSHX/RPUSHX tests: operations on non-existent lists return 0 and do not create keys (length remains 0).
Raft / Network minor
src/raft/src/network.rs
Minor refactor: compute msg_type once for HMAC data construction in authentication helpers; no behavior change.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Redis
  participant Batch
  participant RocksDB

  Client->>Redis: write command (e.g., HSET/ZADD/LPUSH)
  Redis->>Redis: prepare encoded keys & metadata
  Redis->>Batch: create_batch()
  Redis->>Batch: put/delete per ColumnFamilyIndex
  Redis->>Batch: commit()
  alt RocksBatch (standalone)
    Batch->>RocksDB: db.write_opt(WriteBatch, WriteOptions)
    RocksDB-->>Batch: write result
  else BinlogBatch (cluster placeholder)
    Batch-->>Redis: (TODO: Raft/binlog integration)
  end
  Batch-->>Redis: commit result
  Redis-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🧹 Updates

Suggested reviewers

  • AlexStocks
  • marsevilspirit

Poem

🐰 I hop through keys and bundle tight,

nibbling bugs by batching byte,
Rocks or logs, I stack with care,
one small commit — no scattered fare,
a carrot clap for atomic light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: introducing a Batch trait abstraction for atomic write operations, which is the primary focus of all modifications across multiple storage modules.
Linked Issues check ✅ Passed All coding-related objectives from #222 are met: custom batch mechanism provided, RocksBatch for standalone mode implemented, BinlogBatch reserved for cluster mode, and batch trait enables runtime mode-dependent behavior.
Out of Scope Changes check ✅ Passed All changes are in scope: new batch.rs module, Redis.create_batch() factory, refactored WriteBatch usage across redis modules, and test adjustments for LPUSHX/RPUSHX behavior are all necessary for implementing the batch abstraction.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/storage/src/redis_sets.rs (1)

2182-2252: SDIFFSTORE / *store operations are no longer atomic (two commits).

At Line 2239-2247 (and similarly Line 2362-2375), the code commits a “clear destination” batch, then calls self.sadd(...) which commits again. That creates an observable intermediate state (destination empty/partially written) and breaks the typical atomic semantics expected from *STORE.

Suggestion: build one batch that (1) deletes old destination meta + members and (2) writes the new meta + members, then commit once. If you want to reuse sadd, consider adding an internal helper like sadd_into_batch(&mut dyn Batch, ...) that doesn’t commit.

Also applies to: 2309-2377

🤖 Fix all issues with AI agents
In @src/storage/src/batch.rs:
- Around line 47-79: The Batch trait currently exposes put/delete as infallible
and RocksBatch implementation uses assert!/expect! which can panic; change the
trait signatures for put and delete to return Result<(), ErrorType> (or make
them return Result<()> using the crate's common error type), update
RocksBatch::{put, delete} to validate column family lookup without assert/expect
and return Err(...) on invalid CF or other failures, and propagate/store any
internal errors so that commit(self: Box<Self>) returns those errors instead of
panicking; update all call sites to handle the new Result return values and
ensure commit still returns Result<()> with any accumulated error.
- Around line 38-45: Run rustfmt on the new module to resolve the formatting
warning: run `cargo fmt` (or apply rustfmt) for src/storage/src/batch.rs so the
use/import block is properly ordered and spaced (std::sync::Arc;
rocksdb::{BoundColumnFamily, WriteBatch, WriteOptions}; snafu::ResultExt;
crate::error::{Result, RocksSnafu}; crate::ColumnFamilyIndex; engine::Engine).
Ensure no extra blank lines or misaligned imports remain so CI formatting check
passes.
- Around line 166-224: BinlogBatch::commit currently returns Ok(()) while doing
nothing; change it to return an explicit not-implemented error (e.g.,
Err(Error::unimplemented or a suitable crate::error::Error variant) from the
commit method) so callers (including create_batch when it may return
BinlogBatch) cannot acknowledge writes that aren’t persisted; update the commit
implementation in the BinlogBatch impl to construct and return that explicit
error and keep the method body otherwise unchanged until Raft append logic is
implemented.
🧹 Nitpick comments (5)
src/storage/src/redis_strings.rs (1)

2107-2151: Potentially unbounded in-memory key collection for DEL/FLUSHDB paths.

At Line 2110-2151 and 2226-2262, keys are collected into a Vec and then deleted via one batch commit. For large DBs this can spike memory and produce very large WriteBatches.

Consider chunking: delete/commit every N keys (or stream deletes directly into a batch and commit when batch.count() reaches a threshold).

Also applies to: 2226-2262

src/storage/src/redis_lists.rs (2)

319-349: lpop/rpop now apply deletes + meta update in one batch — good.

The new keys_to_delete collection and single batch commit (Line 341-349, 412-420, 483-491) is consistent.

Also applies to: 390-420, 461-491


754-806: Batch delete/put loops are correct; consider writing directly into batch to reduce allocations.

Several paths build Vec<Vec<u8>> and Vec<(Vec<u8>, Vec<u8>)> first (e.g., Line 754-806, 887-937, 1032-1073). Where feasible, you can push operations directly into batch as you compute them to avoid duplicating key/value buffers.

Also applies to: 887-937, 1032-1073

src/storage/src/redis_hashes.rs (2)

112-118: Formatting issue flagged by CI.

The batch operations are correct, but the CI cargo fmt check indicates formatting differences. Run cargo fmt to fix.

🧹 Run formatter
cargo fmt --all

289-303: Helper closure pattern works but creates some duplication.

The create_new_hash closure is duplicated across hset, hmset, hsetnx, hincrby, and hincrbyfloat. While the closures capture method-specific base_meta_key, consider extracting to a shared method that takes base_meta_key as a parameter to reduce duplication. This is a nice-to-have refactor for future.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42cd43d and 0531ae0.

📒 Files selected for processing (8)
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🧰 Additional context used
🧬 Code graph analysis (5)
src/storage/src/redis_sets.rs (1)
src/storage/src/batch.rs (2)
  • new (102-114)
  • new (186-188)
src/storage/src/redis_strings.rs (1)
src/storage/src/batch.rs (2)
  • new (102-114)
  • new (186-188)
src/storage/src/redis_lists.rs (2)
src/storage/src/batch.rs (1)
  • new (102-114)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/redis_hashes.rs (1)
src/storage/src/batch.rs (2)
  • new (102-114)
  • new (186-188)
src/storage/src/redis_zsets.rs (1)
src/storage/src/batch.rs (2)
  • new (102-114)
  • new (186-188)
🪛 GitHub Check: cargo fmt (ubuntu-latest)
src/storage/src/batch.rs

[warning] 40-40:
Diff in /home/runner/work/kiwi/kiwi/src/storage/src/batch.rs

src/storage/src/redis_hashes.rs

[warning] 113-113:
Diff in /home/runner/work/kiwi/kiwi/src/storage/src/redis_hashes.rs


[warning] 297-297:
Diff in /home/runner/work/kiwi/kiwi/src/storage/src/redis_hashes.rs


[warning] 622-622:
Diff in /home/runner/work/kiwi/kiwi/src/storage/src/redis_hashes.rs

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: cargo clippy (macos-latest)
  • GitHub Check: cargo clippy (windows-latest)
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: build and test (windows-latest)
  • GitHub Check: build and test (ubuntu-latest)
  • GitHub Check: cargo clippy (ubuntu-latest)
  • GitHub Check: lint
  • GitHub Check: test-macos
  • GitHub Check: test-windows
  • GitHub Check: test
🔇 Additional comments (19)
src/storage/src/lib.rs (1)

42-43: Batch module wiring + re-export looks good.

mod batch; (Line 42) and pub use batch::{Batch, BinlogBatch, RocksBatch}; (Line 69) are consistent with the new abstraction.

Also applies to: 69-70

src/storage/src/redis_strings.rs (1)

318-325: Batch refactor for string writes is consistent and improves atomicity.

The create_batch() -> put/delete -> commit() pattern applied across setters (e.g., Line 581-588, 635-642, 1063-1079) is consistent and keeps write paths uniform.

Also applies to: 389-396, 581-588, 635-642, 698-705, 759-766, 831-838, 1063-1079

src/storage/src/redis_sets.rs (1)

151-166: Batch usage in set mutators (sadd/srem/spop/smove) looks coherent.

The conversions to create_batch() with MetaCF + SetsDataCF operations and a single commit() (e.g., Line 151-166, 494-512, 783-813) match the intended abstraction.

Also applies to: 494-512, 627-647, 783-813

src/storage/src/redis_lists.rs (1)

105-108: List push_core() batching approach is clean and keeps meta+data updates in one commit.

Collecting deletes/puts and committing them together (Line 236-246) aligns well with the new Batch abstraction and should preserve atomicity for each list mutation.

Also applies to: 236-246

src/storage/src/redis_zsets.rs (9)

30-30: Import cleanup looks good.

The removal of WriteBatch from the import is consistent with the migration to the new batch abstraction.


99-166: Batch abstraction correctly applied for existing zset updates.

The migration from direct WriteBatch to the new batch abstraction maintains atomicity - all score key deletions, member/score insertions, and meta updates are committed together.


169-189: New zset creation path correctly uses batch abstraction.

The batch lifecycle (create → put → commit) is properly implemented for the new zset creation path.


357-376: Score increment batch operations are correct.

The batch properly handles the atomic update of: delete old score key → put new member value → put new score key → commit.


791-843: Member removal batch operations are well-structured.

The batch correctly handles multi-member deletion with proper meta update logic (delete meta if count reaches zero, otherwise update count). Statistics update is appropriately placed after successful commit.


1510-1528: Lex range removal uses correct batch pattern.

The collect-then-batch approach is efficient, and all deletions plus meta updates are atomic.


1629-1647: Rank range removal batch operations are correct.

Consistent pattern with other range removal methods.


1728-1746: Score range removal batch operations are correct.

Consistent and correct batch usage pattern.


1177-1240: Two-phase batch approach for store operations is acceptable.

The destination cleanup uses one batch (lines 1197-1227), then results are added via zadd which uses its own batch. This maintains atomicity within each phase. The lock is correctly released before calling zadd to prevent deadlock.

Note: In edge cases where the first batch commits but zadd fails, the destination would be cleared but not populated. This matches Redis behavior where a failure partway through leaves partial state.

src/storage/src/redis_hashes.rs (6)

23-23: Import cleanup is consistent with batch migration.

Removed WriteBatch import as expected for the batch abstraction migration.


335-339: Batch operations for stale hash reinit are correct.

The batch properly updates both meta and data in a single atomic commit.


696-704: Conditional meta update is correct but could be simplified.

The meta is only updated when new_fields_count > 0, which is correct. However, the batch is always created and committed even when no new fields are added (just updates to existing fields). This is fine for correctness but creates a batch commit for updates-only scenarios.


903-914: Integer overflow protection and batch operation are correct.

The checked_add prevents overflow, and the batch commit properly persists the incremented value.


1032-1046: Float overflow protection and batch operation are correct.

The is_finite() check properly guards against NaN/Infinity results before committing.


774-778: HSETNX batch operations are correct.

Atomic commit of meta and data for stale hash reinitialization.

@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch from 0531ae0 to b86f898 Compare January 9, 2026 08:16
@Tsukikage7 Tsukikage7 changed the title feat(storage): 添加 Batch trait 抽象层支持原子写操作 feat(storage): add Batch trait abstraction for atomic write operations Jan 9, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/storage/src/redis_sets.rs (1)

2206-2276: Atomicity regression risk in *STORE: destination clear and repopulate happen in multiple commits.

sdiffstore() (and clear_and_store_set() used by sinterstore/sunionstore) currently:

  1. deletes existing destination keys and commits
  2. calls sadd() which commits again

This opens race windows (and intermediate visibility) and can violate “single command is atomic” expectations, especially since reads typically don’t take locks.

Suggested fix: do clear + repopulate in one batch commit
- // Write the batch to clear destination first
- let mut batch = self.create_batch()?;
- ...
- batch.commit()?;
-
- // Now add the new members
- let added = self.sadd(destination, &member_refs)?;
- Ok(added)
+ // Clear + write new meta + write new members in ONE batch
+ let mut batch = self.create_batch()?;
+ for key_to_del in &keys_to_delete {
+     batch.delete(ColumnFamilyIndex::SetsDataCF, key_to_del)?;
+ }
+ if should_delete_meta {
+     batch.delete(ColumnFamilyIndex::MetaCF, &dest_base_meta_key)?;
+ }
+
+ // Build and write destination meta + members here (avoid calling sadd())
+ // - create new BaseMetaValue(DataType::Set) with count=members.len()
+ // - get version from ParsedSetsMetaValue::initial_meta_value()
+ // - for each member: MemberDataKey::new(destination, version, member).encode() and batch.put(...)
+
+ batch.commit()?;
+ Ok(members.len() as i32)

Also applies to: 2333-2401

🤖 Fix all issues with AI agents
In @src/storage/src/redis.rs:
- Around line 320-360: The code relies on positional indexing of self.handles
(via get_cf_handle/ColumnFamilyIndex) but initialization used filter() which can
remove missing CFs and shift positions, breaking the mapping; change the
implementation to use a stable name-based mapping instead: during DB open
populate a fixed-size Vec<Option<Arc<rocksdb::BoundColumnFamily<'_>>>> or a
HashMap<ColumnFamilyIndex, Arc<...>> keyed by ColumnFamilyIndex (or CF name) so
missing CFs are represented as None rather than removed, update get_cf_handle to
return the handle by lookup (not by index into a filtered Vec), and ensure
create_batch collects handles by calling the new get_cf_handle for each
ColumnFamilyIndex (MetaCF, HashesDataCF, SetsDataCF, ListsDataCF, ZsetsDataCF,
ZsetsScoreCF) so ordering/invariants are preserved.
🧹 Nitpick comments (4)
src/storage/src/batch.rs (1)

95-199: RocksBatch CF index validation is good; consider tightening diagnostics + avoiding repeated custom Location building.

  • The “max” in the bounds error reads like an index but is currently len; consider len.saturating_sub(1).
  • You can simplify the error location to snafu::location!() (and keep the message).
src/storage/src/redis_lists.rs (1)

718-806: Nice consolidation to single-commit mutations; consider avoiding pre-staging when not needed.

Several paths build Vec of keys/puts first and then replay into batch.*. If borrow/lifetime constraints permit, writing directly into batch as you compute keys would save memory and copies on large lists.

Also applies to: 887-937, 1032-1073

src/storage/src/redis_strings.rs (1)

2110-2151: Potential memory spike: del_key/flush_db collect all keys before deleting.

For large datasets, keys_to_delete can become huge. Consider chunked deletion (e.g., commit every N ops and batch.clear()), or implement a purpose-built “delete range/prefix” API in the engine later.

Also applies to: 2226-2261

src/storage/src/redis_hashes.rs (1)

294-314: Consider extracting repeated helper pattern.

The create_new_hash helper closure appears with minor variations across multiple methods (also in hsetnx at lines 780-800, hincrby at lines 905-926, and hincrbyfloat at lines 1061-1082). While the current implementation works correctly and maintains atomicity, extracting this to a shared generic helper method could reduce duplication.

Example consolidation approach

A shared method could accept a value encoder:

fn create_new_hash_with_field<F>(
    &self,
    key: &[u8],
    base_meta_key: &[u8],
    field: &[u8],
    encode_value: F,
) -> Result<()>
where
    F: FnOnce() -> Vec<u8>,
{
    let mut hashes_meta = HashesMetaValue::new(Bytes::copy_from_slice(&1u64.to_le_bytes()));
    hashes_meta.inner.data_type = DataType::Hash;
    let version = hashes_meta.update_version();

    let data_key = MemberDataKey::new(key, version, field);
    let data_value = BaseDataValue::new(encode_value());

    let mut batch = self.create_batch()?;
    batch.put(ColumnFamilyIndex::MetaCF, base_meta_key, &hashes_meta.encode())?;
    batch.put(ColumnFamilyIndex::HashesDataCF, &data_key.encode()?, &data_value.encode())?;
    batch.commit()
}

Then call sites would become: create_new_hash_with_field(key, &base_meta_key, field, || value.to_string().into_bytes())?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0531ae0 and b86f898.

📒 Files selected for processing (8)
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/storage/src/lib.rs
  • src/storage/src/redis_zsets.rs
🧰 Additional context used
🧬 Code graph analysis (5)
src/storage/src/redis_sets.rs (1)
src/storage/src/batch.rs (2)
  • new (113-125)
  • new (221-223)
src/storage/src/redis_lists.rs (2)
src/storage/src/batch.rs (1)
  • new (113-125)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/redis_strings.rs (1)
src/storage/src/batch.rs (2)
  • new (113-125)
  • new (221-223)
src/storage/src/redis_hashes.rs (1)
src/storage/src/batch.rs (2)
  • new (113-125)
  • new (221-223)
src/storage/src/batch.rs (2)
src/storage/src/redis.rs (1)
  • new (115-149)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test-windows
  • GitHub Check: cargo clippy (macos-latest)
🔇 Additional comments (13)
src/storage/src/batch.rs (2)

47-90: Batch trait shape looks good for “commit consumes batch” flow.

commit(self: Box<Self>) aligns with Redis::create_batch() -> Box<dyn Batch> and avoids post-commit reuse footguns.


201-265: BinlogBatch correctly fails commit to avoid silent data loss.

Returning an error from commit() is the right default until Raft integration lands.

src/storage/src/redis_lists.rs (2)

105-246: push_core batching integration looks consistent (data CF ops + meta update in one commit).

Order of operations (deletes, puts, meta put, commit) is coherent and keeps the metadata update in the same atomic commit.


317-349: Pop paths correctly batch element deletions with metadata update.

Keeping meta written even when empty matches the comment/intent and avoids breaking lpushx/rpushx flows.

Also applies to: 388-420, 459-491

src/storage/src/redis_strings.rs (1)

318-325: Batch-based writes are consistently applied across string mutations.

These conversions preserve “single logical operation → single commit” and fit the new Batch abstraction well.

Also applies to: 389-396, 581-588, 635-642, 698-705, 759-766, 831-838, 1063-1079, 1137-1153, 1212-1219, 1284-1291, 1836-1884, 1938-1945, 1979-1986

src/storage/src/redis_sets.rs (1)

151-169: Core set mutations migrated cleanly to Batch (single commit).

These paths keep metadata + member updates together and look correct.

Also applies to: 472-520, 634-659, 795-837

src/storage/src/redis_hashes.rs (7)

86-122: LGTM! Efficient collect-then-batch pattern.

The refactoring correctly implements the batch abstraction with a two-phase approach: first collecting keys that need deletion (lines 86-99), then performing all operations in a single atomic batch (lines 112-121). This ensures consistency between the metadata count and actual deletions.


346-407: LGTM! Batch operations correctly handle all branches.

The batch abstraction is properly applied across all three logical branches:

  • Stale/empty hash initialization (lines 346-357)
  • Existing field updates (lines 376-382)
  • New field additions (lines 396-407)

Each branch maintains atomicity guarantees and correctly coordinates metadata updates with data changes.


648-664: LGTM! Efficient multi-field batch pattern.

The helper correctly consolidates all field-value pairs into a single batch operation, writing the metadata once followed by all data entries. This is more efficient than per-field batches and maintains atomicity for the entire multi-set operation.


716-755: LGTM! Two-phase batch strategy for existing hashes.

The collect-then-batch pattern (lines 716-730 collect, lines 743-754 batch) correctly:

  • Checks field existence before batching to determine new field count
  • Accumulates all writes before committing
  • Conditionally updates metadata only when new fields are added
  • Commits everything atomically in a single batch

832-876: LGTM! Batch operations correctly implement set-if-not-exists semantics.

Both the stale hash reinitialization (lines 832-843) and new field addition (lines 865-876) paths correctly use atomic batch operations to coordinate metadata and data updates.


959-1032: LGTM! Atomic increment operations with proper overflow handling.

The batch-based implementation correctly handles:

  • Integer overflow detection before committing (line 993)
  • Atomic updates for existing fields (lines 1000-1006)
  • Coordinated meta+data writes for new fields (lines 1021-1032)

All branches maintain transactional consistency through the batch abstraction.


1115-1191: LGTM! Atomic float increment with proper validation.

The implementation correctly:

  • Validates float results are finite before committing (lines 1148-1154)
  • Uses atomic batch updates for existing fields (lines 1159-1165)
  • Coordinates meta+data writes for new fields (lines 1180-1191)

The batch abstraction maintains consistency across all paths.

@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch 2 times, most recently from 5b92f55 to 0fd2c70 Compare January 9, 2026 08:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
src/storage/src/redis_zsets.rs (2)

59-65: Fix score equality check in zadd (currently compares the wrong value).

This condition is wrong: it checks abs(sm.score) instead of abs(existing_score - sm.score), so it will “skip” updates incorrectly when sm.score is near 0.

Proposed fix
-                                if (existing_score, sm.score).1.abs() < f64::EPSILON {
+                                if (existing_score - sm.score).abs() < f64::EPSILON {
                                     // Score is the same, skip
                                     continue;
                                 } else {

Also applies to: 99-180, 188-213


1234-1265: zset_store_operation: delete+recreate is not atomic vs concurrent writers (lock gap + two commits).

You delete destination in one commit, then (after releasing the lock) call zadd() which does a second commit. A concurrent writer can observe/modify the destination in-between. Consider an internal “zadd without lock” (called while holding the destination lock) or building the destination contents in the same batch/commit.

src/storage/src/redis_sets.rs (3)

472-520: Use saturating_sub when decrementing set counts.

If metadata is inconsistent/corrupted, set_meta.count() - removed_count can underflow. Other modules already use saturating_sub.

Proposed fix
-            let new_count = set_meta.count() - removed_count as u64;
+            let new_count = set_meta.count().saturating_sub(removed_count as u64);

2206-2271: sdiffstore: destination is cleared without a destination lock (race vs concurrent ops).

You clear destination keys/meta via batch without holding the destination key lock, then later sadd() takes the lock. This breaks the per-key lock contract used elsewhere (read-modify-write ops won’t synchronize with the clear).

Sketch fix (lock destination during clear phase)
 pub fn sdiffstore(&self, destination: &[u8], keys: &[&[u8]]) -> Result<i32> {
+        let dest_str = String::from_utf8_lossy(destination).to_string();
+        let _dest_lock = ScopeRecordLock::new(self.lock_mgr.as_ref(), &dest_str);
         ...
 }

2333-2401: clear_and_store_set: Fix self-deadlock caused by nested lock acquisition

clear_and_store_set holds a ScopeRecordLock on the destination key, then calls self.sadd(destination, &member_refs) which attempts to acquire the same lock again. Since parking_lot::Mutex is not re-entrant, this causes a self-deadlock. Refactor to either:

  1. Extract the core sadd logic into a separate internal method that does not acquire the lock, then call it from both sadd and clear_and_store_set while holding the lock once
  2. Perform the sadd operations directly within clear_and_store_set without calling sadd
src/storage/src/redis_strings.rs (3)

1106-1155: msetnx existence check is not type-agnostic (can overwrite live non-string keys).

The loop checks only BaseKey (string) entries in MetaCF. If a key exists as hash/set/zset/list (stored under BaseMetaKey), msetnx can incorrectly proceed and overwrite it—contradicting the comment “any live key (any type) blocks the batch”.

One possible fix: reuse `get_key_type` for existence
 pub fn msetnx(&self, kvs: &[(Vec<u8>, Vec<u8>)]) -> Result<bool> {
-        let db = self.db.as_ref().context(OptionNoneSnafu {
-            message: "db is not initialized".to_string(),
-        })?;
-
-        let _cf = self
-            .get_cf_handle(ColumnFamilyIndex::MetaCF)
-            .context(OptionNoneSnafu {
-                message: "cf is not initialized".to_string(),
-            })?;
-
-        // Check if any key exists and is not expired
-        for (key, _) in kvs {
-            let string_key = BaseKey::new(key);
-
-            match db
-                .get_opt(&string_key.encode()?, &self.read_options)
-                .context(RocksSnafu)?
-            {
-                Some(val) => {
-                    let string_value = ParsedStringsValue::new(&val[..])?;
-                    if !string_value.is_stale() {
-                        return Ok(false);
-                    }
-                }
-                None => {}
-            }
-        }
+        // Check if any *live* key exists (any type blocks MSETNX, Redis-compatible).
+        for (key, _) in kvs {
+            if self.get_key_type(key).is_ok() {
+                return Ok(false);
+            }
+        }

2226-2261: flush_db accumulates unbounded batches on large databases; implement chunked deletes (commit every N keys) or add delete_range support to the Batch trait.

The current implementation iterates all keys across multiple column families, collects them into a single Vec, then adds all delete operations to a single WriteBatch before committing. On large databases, this causes:

  • Unnecessary memory allocation for the entire key vector
  • Single massive write batch with thousands/millions of operations
  • Increased WAL overhead and compaction pressure

RocksDB 0.23.0 provides delete_range_cf() for efficient range deletes, but it's not exposed by the Batch trait. Chunking deletes into smaller batches (commit every 10k–100k keys) would be a low-risk fix; alternatively, extend the Batch trait to support delete_range().


2110-2151: Fix prefix-scan logic and limit batch accumulation in del_key.

The current implementation has two issues:

  1. Incorrect prefix matching: BaseKey::encode() includes the 16-byte reserve2 suffix, but composite keys (HashesDataCF, SetsDataCF, etc.) store version bytes immediately after the encoded user key. The check k.starts_with(&encoded) will not match these keys because the byte sequence after the user key is [version][data], not [reserve2]. Use encode_seek_key() or a prefix without reserve2 instead (see redis_hashes.rs:225 for the correct pattern).

  2. Unbounded memory accumulation: Collecting all matched keys into keys_to_delete before deletion can exhaust memory for large composite types. Build and commit the batch incrementally instead (see redis_multi.rs:462-468 for the pattern).

src/storage/src/redis_hashes.rs (1)

86-122: Fix clippy/lint failures: avoid &temp_vec() / needless borrow-to-deref in batch.put(...).

CI is failing with “this expression creates a reference which is immediately dereferenced” at Line 119/350/400/698/751/836/869/963/1025/1119. The typical trigger here is passing &some_vec (or &some_fn_returning_vec()) where &[u8] is expected. Bind encoded values and pass slices.

Concrete fix pattern (apply to all reported lines)
-                    batch.put(
-                        ColumnFamilyIndex::MetaCF,
-                        &base_meta_key,
-                        &meta_val.encoded(),
-                    )?;
+                    let encoded_meta = meta_val.encoded();
+                    batch.put(
+                        ColumnFamilyIndex::MetaCF,
+                        base_meta_key.as_slice(),
+                        encoded_meta.as_slice(),
+                    )?;
-            batch.put(
-                ColumnFamilyIndex::MetaCF,
-                &base_meta_key,
-                &hashes_meta.encode(),
-            )?;
+            let encoded_meta = hashes_meta.encode();
+            batch.put(
+                ColumnFamilyIndex::MetaCF,
+                base_meta_key.as_slice(),
+                encoded_meta.as_slice(),
+            )?;
-            batch.put(
-                ColumnFamilyIndex::HashesDataCF,
-                &data_key.encode()?,
-                &data_value.encode(),
-            )?;
+            let encoded_key = data_key.encode()?;
+            let encoded_val = data_value.encode();
+            batch.put(
+                ColumnFamilyIndex::HashesDataCF,
+                encoded_key.as_slice(),
+                encoded_val.as_slice(),
+            )?;

Also applies to: 293-314, 346-358, 376-383, 395-408, 637-755, 779-877, 904-1033, 1060-1192

src/storage/src/redis_lists.rs (1)

1080-1154: rpoplpush is not crash-atomic (two commits); consider a single batch commit spanning both keys.

Even with both key locks held, rpop_internal commits first and push_core commits second; a crash between them can lose/misplace an element. With the new Batch abstraction, it should be possible to accumulate deletes/puts for both source+destination into one batch and commit once.

Also applies to: 364-429, 43-255

🤖 Fix all issues with AI agents
In @src/storage/src/batch.rs:
- Around line 56-90: The out-of-bounds error messages for ColumnFamilyIndex use
self.cf_handles.len() as the "max index" but the true maximum valid index is
self.cf_handles.len() - 1; update the error construction in the code paths that
validate cf_idx (referencing cf_handles and the Redis::create_batch() usage) to
report the maximum as self.cf_handles.len().saturating_sub(1) (or handle empty
cf_handles explicitly) so the message shows the correct highest valid index
instead of the length.
- Around line 201-264: The code relies on commented-out selection logic and can
accidentally return a BinlogBatch; add an explicit guard in Redis and
create_batch(): add a bool field like `cluster_mode` to the `Redis` struct (or
use a feature flag), then update `create_batch()` to only construct/return
`BinlogBatch` when `cluster_mode` is true and the required Raft wiring (e.g.,
append log callback) is present; otherwise always return `RocksBatch` and if
`cluster_mode` is true but append-log wiring is missing, return an immediate
error or panic rather than creating a BinlogBatch (leave `BinlogBatch::commit()`
intentional error as a secondary safeguard).

In @src/storage/src/redis_lists.rs:
- Around line 341-349: The code inconsistently handles empty lists:
lpop/rpop/rpop_internal currently preserve list metadata when the list becomes
empty while ltrim/lrem delete it, causing differing "key exists" and
lpushx/rpushx behavior; make this consistent by changing the ltrim and lrem code
paths to preserve metadata instead of deleting the MetaCF entry when a list
becomes empty (or alternatively change lpop/rpop to delete if you prefer that
semantics), updating the batch operations in the affected functions (ltrim,
lrem, and their internal helpers) so they write the parsed_meta.value() to
ColumnFamilyIndex::MetaCF rather than issuing batch.delete, and ensure the same
approach is applied to all referenced code paths (including the rpop_internal
and any other empty-list branches) so key-exists semantics match across
operations.

In @src/storage/src/redis_strings.rs:
- Around line 1063-1079: The mset implementation currently batches puts and
commits without acquiring per-key locks, which can race with ops that rely on
ScopeRecordLock (e.g., incr_decr); modify the mset function to first collect and
sort the keys (use BaseKey::new(key).encode() or the key strings), acquire a
ScopeRecordLock for each key in that sorted order (holding all locks), then
perform create_batch()/batch.put(...) for each kv and finally batch.commit(),
releasing locks after commit; ensure lock acquisition is exception-safe (release
on error) and reference ScopeRecordLock, mset, create_batch, batch.put, and
batch.commit in your change.
🧹 Nitpick comments (3)
src/raft/src/network.rs (1)

386-397: Extract duplicated message type computation into a helper method.

The match expression that computes msg_type is duplicated between add_authentication and verify_authentication. This violates the DRY principle and creates a maintenance risk—if the message type representation needs to change, both locations must be updated consistently.

♻️ Proposed refactor to eliminate duplication

Add a private helper method to MessageEnvelope:

impl MessageEnvelope {
    /// Get the message type representation for HMAC computation
    fn message_type_for_hmac(&self) -> String {
        match &self.message {
            RaftMessage::AppendEntries(_) => "AppendEntries".to_string(),
            RaftMessage::AppendEntriesResponse(_) => "AppendEntriesResponse".to_string(),
            RaftMessage::Vote(_) => "Vote".to_string(),
            RaftMessage::VoteResponse(_) => "VoteResponse".to_string(),
            RaftMessage::InstallSnapshot(_) => "InstallSnapshot".to_string(),
            RaftMessage::InstallSnapshotResponse(_) => "InstallSnapshotResponse".to_string(),
            RaftMessage::Heartbeat { from, term } => format!("Heartbeat:{}:{}", from, term),
            RaftMessage::HeartbeatResponse { from, success } => {
                format!("HeartbeatResponse:{}:{}", from, success)
            }
        }
    }
}

Then update both methods to use the helper:

 pub fn add_authentication(&mut self, auth: &NodeAuth) -> RaftResult<()> {
-    // Use a simplified message representation for HMAC
-    let msg_type = match &self.message {
-        RaftMessage::AppendEntries(_) => "AppendEntries".to_string(),
-        RaftMessage::AppendEntriesResponse(_) => "AppendEntriesResponse".to_string(),
-        RaftMessage::Vote(_) => "Vote".to_string(),
-        RaftMessage::VoteResponse(_) => "VoteResponse".to_string(),
-        RaftMessage::InstallSnapshot(_) => "InstallSnapshot".to_string(),
-        RaftMessage::InstallSnapshotResponse(_) => "InstallSnapshotResponse".to_string(),
-        RaftMessage::Heartbeat { from, term } => format!("Heartbeat:{}:{}", from, term),
-        RaftMessage::HeartbeatResponse { from, success } => {
-            format!("HeartbeatResponse:{}:{}", from, success)
-        }
-    };
+    let msg_type = self.message_type_for_hmac();
     let data_for_hmac = format!(
         "{}:{}:{}:{}:{}",
         self.message_id, self.from, self.to, self.timestamp, msg_type
 pub fn verify_authentication(&self, auth: &NodeAuth) -> bool {
     if let Some(ref expected_hmac) = self.hmac {
-        // Recreate the same data format used for HMAC generation
-        let msg_type = match &self.message {
-            RaftMessage::AppendEntries(_) => "AppendEntries".to_string(),
-            RaftMessage::AppendEntriesResponse(_) => "AppendEntriesResponse".to_string(),
-            RaftMessage::Vote(_) => "Vote".to_string(),
-            RaftMessage::VoteResponse(_) => "VoteResponse".to_string(),
-            RaftMessage::InstallSnapshot(_) => "InstallSnapshot".to_string(),
-            RaftMessage::InstallSnapshotResponse(_) => "InstallSnapshotResponse".to_string(),
-            RaftMessage::Heartbeat { from, term } => format!("Heartbeat:{}:{}", from, term),
-            RaftMessage::HeartbeatResponse { from, success } => {
-                format!("HeartbeatResponse:{}:{}", from, success)
-            }
-        };
+        let msg_type = self.message_type_for_hmac();
         let data_for_hmac = format!(
             "{}:{}:{}:{}:{}",
             self.message_id, self.from, self.to, self.timestamp, msg_type

Also applies to: 411-422

src/storage/src/lib.rs (1)

42-43: Public API expansion: consider feature-gating or sealing BinlogBatch until it’s real.

Re-exporting BinlogBatch makes it part of the stable surface; if it’s intentionally a placeholder, consider hiding behind a cluster feature or documenting “unstable/experimental” to avoid long-term API constraints.

Also applies to: 69-70

src/storage/src/redis_hashes.rs (1)

293-314: Consider de-duplicating the repeated create_new_hash closures.

The same “init meta + write 1+ fields via batch” logic is repeated in hset, hmset, hsetnx, hincrby, hincrbyfloat. A small private helper (or a generic helper taking an iterator of (field,value_bytes)) would cut repetition and reduce risk of divergence.

Also applies to: 637-665, 779-800, 904-927, 1060-1082

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b86f898 and 5b92f55.

📒 Files selected for processing (9)
  • src/raft/src/network.rs
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/storage/src/redis.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/storage/src/redis_lists.rs (3)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/storage_impl.rs (1)
  • key (150-150)
src/storage/src/base_key_format.rs (1)
  • key (88-90)
src/storage/src/redis_zsets.rs (1)
src/storage/src/batch.rs (2)
  • new (113-125)
  • new (221-223)
src/storage/src/batch.rs (2)
src/storage/src/redis.rs (1)
  • new (115-149)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/redis_strings.rs (1)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
🪛 GitHub Check: cargo clippy (ubuntu-latest)
src/storage/src/redis_hashes.rs

[failure] 119-119:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 350-350:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 400-400:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 698-698:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 751-751:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 836-836:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 869-869:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 963-963:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 1025-1025:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 1119-1119:
this expression creates a reference which is immediately dereferenced by the compiler

🪛 GitHub Check: lint
src/storage/src/redis_hashes.rs

[failure] 119-119:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 350-350:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 400-400:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 698-698:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 751-751:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 836-836:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 869-869:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 963-963:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 1025-1025:
this expression creates a reference which is immediately dereferenced by the compiler


[failure] 1119-1119:
this expression creates a reference which is immediately dereferenced by the compiler

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test-macos
  • GitHub Check: test
  • GitHub Check: test-windows
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: build and test (windows-latest)
🔇 Additional comments (8)
src/storage/src/redis_zsets.rs (4)

29-31: Import change looks fine.


321-327: zincrby batch migration looks correct (delete old score key + write new pair + commit).

Also applies to: 383-410


792-877: zrem batching/commit-on-change looks good.


1473-1574: Range-removal paths migrated cleanly to batch+commit.

Also applies to: 1670-1697, 1712-1800

src/storage/src/redis_sets.rs (3)

25-26: Import change looks fine.


151-169: sadd batch refactor looks correct (single commit for meta + members).


635-659: spop / smove batching looks good; multi-key lock ordering in smove is a nice touch.

Also applies to: 795-837

src/storage/src/redis_strings.rs (1)

318-325: Single-key write paths migrated cleanly to batch+commit.

Also applies to: 389-396, 581-588, 635-642, 698-705, 759-766, 831-838, 1212-1219, 1284-1291, 1836-1884, 1938-1945, 1979-1986

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/raft/src/network.rs (1)

383-405: Critical: HMAC payload doesn’t cover message body (integrity bypass).

Right now msg_type is the only commitment to self.message for most variants, so an attacker can alter (e.g.) AppendEntries fields without changing data_for_hmac, and the HMAC would still verify. Please include the full RaftMessage (or a digest of it) in the authenticated bytes, and centralize the construction to avoid drift.

Proposed fix (commit HMAC to full message content; remove duplicated match)
 impl MessageEnvelope {
+    fn hmac_payload(&self) -> RaftResult<Vec<u8>> {
+        // IMPORTANT: commit to the full message content (not just its variant name)
+        bincode::serialize(&(self.message_id, self.from, self.to, self.timestamp, &self.message))
+            .map_err(|e| {
+                RaftError::Serialization(serde_json::Error::io(std::io::Error::new(
+                    std::io::ErrorKind::InvalidData,
+                    e.to_string(),
+                )))
+            })
+    }
+
     /// Add authentication to the message
     pub fn add_authentication(&mut self, auth: &NodeAuth) -> RaftResult<()> {
-        // Create serializable data without HMAC for authentication
-        // Use a simplified message representation for HMAC
-        let msg_type = match &self.message {
-            RaftMessage::AppendEntries(_) => "AppendEntries".to_string(),
-            RaftMessage::AppendEntriesResponse(_) => "AppendEntriesResponse".to_string(),
-            RaftMessage::Vote(_) => "Vote".to_string(),
-            RaftMessage::VoteResponse(_) => "VoteResponse".to_string(),
-            RaftMessage::InstallSnapshot(_) => "InstallSnapshot".to_string(),
-            RaftMessage::InstallSnapshotResponse(_) => "InstallSnapshotResponse".to_string(),
-            RaftMessage::Heartbeat { from, term } => format!("Heartbeat:{}:{}", from, term),
-            RaftMessage::HeartbeatResponse { from, success } => {
-                format!("HeartbeatResponse:{}:{}", from, success)
-            }
-        };
-        let data_for_hmac = format!(
-            "{}:{}:{}:{}:{}",
-            self.message_id, self.from, self.to, self.timestamp, msg_type
-        );
-
-        self.hmac = Some(auth.generate_hmac(data_for_hmac.as_bytes())?);
+        let payload = self.hmac_payload()?;
+        self.hmac = Some(auth.generate_hmac(&payload)?);
         Ok(())
     }

     /// Verify message authentication
     pub fn verify_authentication(&self, auth: &NodeAuth) -> bool {
         if let Some(ref expected_hmac) = self.hmac {
-            // Recreate the same data format used for HMAC generation
-            let msg_type = match &self.message {
-                RaftMessage::AppendEntries(_) => "AppendEntries".to_string(),
-                RaftMessage::AppendEntriesResponse(_) => "AppendEntriesResponse".to_string(),
-                RaftMessage::Vote(_) => "Vote".to_string(),
-                RaftMessage::VoteResponse(_) => "VoteResponse".to_string(),
-                RaftMessage::InstallSnapshot(_) => "InstallSnapshot".to_string(),
-                RaftMessage::InstallSnapshotResponse(_) => "InstallSnapshotResponse".to_string(),
-                RaftMessage::Heartbeat { from, term } => format!("Heartbeat:{}:{}", from, term),
-                RaftMessage::HeartbeatResponse { from, success } => {
-                    format!("HeartbeatResponse:{}:{}", from, success)
-                }
-            };
-            let data_for_hmac = format!(
-                "{}:{}:{}:{}:{}",
-                self.message_id, self.from, self.to, self.timestamp, msg_type
-            );
-
-            auth.verify_hmac(data_for_hmac.as_bytes(), expected_hmac)
+            let payload = match self.hmac_payload() {
+                Ok(v) => v,
+                Err(_) => return false,
+            };
+            auth.verify_hmac(&payload, expected_hmac)
         } else {
             false
         }
     }
 }

Also applies to: 408-432

src/storage/src/redis_sets.rs (1)

2375-2398: Potential atomicity issue: two separate batch commits.

Similar to sdiffstore, this helper method performs two sequential batch commits: clearing the destination (lines 2375-2383 or 2387-2394) and adding members via sadd (line 2398). This breaks atomicity for sinterstore and sunionstore operations that depend on this helper.

Both clearing and adding operations should be combined into a single atomic batch to prevent partial state if the second operation fails.

src/storage/src/redis_zsets.rs (3)

59-65: Bug: score equality check in zadd compares the wrong value.

if (existing_score, sm.score).1.abs() < f64::EPSILON evaluates sm.score.abs(), not (|existing_score - sm.score|). This makes “no-op when score unchanged” behave incorrectly and triggers unnecessary delete+put.

Proposed fix
-                                if (existing_score, sm.score).1.abs() < f64::EPSILON {
+                                if (existing_score - sm.score).abs() < f64::EPSILON {
                                     // Score is the same, skip
                                     continue;
                                 } else {

Also applies to: 101-180, 188-213


321-327: zincrby: batch update is good, but lock-dropping + re-calling zadd can break atomicity under concurrency.

When meta is invalid/missing, the function drops _lock then calls zadd, allowing another writer to interleave. Consider an internal zadd_no_lock (or a shared helper that accepts “lock already held”) to keep the whole operation linearizable.

Also applies to: 383-410


1234-1265: zinterstore/zunionstore: destination delete + later zadd is not atomic.

You delete destination under a lock, release the lock, then later zadd repopulates. Another operation on destination can slip in between. If strict Redis semantics matter here, this should be refactored to a single locked write plan.

src/storage/src/redis_lists.rs (1)

319-334: Metadata decrement should match actual keys found; add validation or assertions when data keys are missing.

keys_to_delete is populated only when get_cf returns a value, but pop_count unconditionally decrements the metadata count and indices. If any expected data key is absent, the count will drift even though fewer items were actually popped. The same issue exists in RPOP, RPOPLPUSH, and LREM. Either assert that all keys exist (if guaranteed by design) or adjust the count decrement to match keys_to_delete.len() or result.len().

Also applies to: 341-349, 390-405, 412-420, 461-476, 483-491

🧹 Nitpick comments (7)
src/storage/src/redis_strings.rs (3)

581-588: Batch migration for single-key writes looks consistent (put/delete + commit).

Nice mechanical refactor: each mutation now commits through the Batch abstraction, aligning with the PR goal (standalone vs cluster).

Consider a tiny helper to reduce repetition/error-surface (create batch → put MetaCF → commit), since this pattern appears many times in this file.

Also applies to: 635-642, 698-705, 759-766, 831-838, 1212-1219, 1284-1291, 1429-1436, 1836-1843, 1856-1863, 1877-1884, 1938-1945, 1979-1986


2110-2151: del_key: good “collect then batch delete”, but watch memory/latency for large composite keys.

Collecting all derived keys across CFs into keys_to_delete can be large and then written as one huge batch; consider chunked commits (e.g., commit every N deletions) to cap memory and avoid oversized WriteBatch.


2226-2261: flush_db: same batching concern—consider chunking or CF-level primitives.

For large DBs, collecting all keys into memory is costly. If RocksDB drop/truncate CF isn’t an option here, chunked batch commits would at least bound memory.

src/storage/src/redis_lists.rs (2)

718-719: ltrim: meta delete vs update is clear; consider chunking deletes for big lists.

Logic is readable and the batch write is clean. For very large lists, the delete-key accumulation can be big; chunking commits would reduce peak memory.

Also applies to: 754-757, 764-767, 773-774, 781-782, 791-792, 794-806


888-892: lrem: batch rewrite is fine; watch O(N) memory for large lists.

Reads all elements and rewrites the whole list; if that’s already the intended approach, the batch refactor is consistent. If not, this is a hotspot to optimize later.

Also applies to: 898-904, 914-915, 923-937

src/storage/src/redis_hashes.rs (2)

293-314: hset: batch writes look good; consider deduplicating the “create_new_hash” logic across methods.

The inline closure pattern repeats in hset, hmset, hsetnx, hincrby, hincrbyfloat. A small private helper (e.g., fn init_hash_with_fields(&self, ...)) would reduce drift risk.

Also applies to: 346-358, 376-383, 395-408, 414-416


904-926: Error messages for numeric overflow/underflow should be consistent across string and hash operations.

There are inconsistencies in error messaging:

  • Integer overflow: hashes report "integer overflow" (line 994) while strings report "increment or decrement would overflow" (strings.rs:1203)
  • Non-integer parse errors: hashes report "hash value is not an integer" (line 987) while strings report "value is not an integer or out of range" (strings.rs:1191)
  • Float parse errors and NaN/Infinity checks are already aligned

For consistency with Redis semantics and client expectations, align these messages across both data types.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b86f898 and 0fd2c70.

📒 Files selected for processing (9)
  • src/raft/src/network.rs
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/batch.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/storage/src/redis_strings.rs (2)
src/storage/src/redis.rs (1)
  • new (115-149)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/redis_hashes.rs (3)
src/storage/src/base_meta_value_format.rs (2)
  • new (56-63)
  • new (103-147)
src/storage/src/base_key_format.rs (1)
  • key (88-90)
src/storage/src/redis_multi.rs (1)
  • exists (130-152)
src/storage/src/redis_zsets.rs (1)
src/storage/src/batch.rs (2)
  • new (113-125)
  • new (221-223)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: cargo clippy (macos-latest)
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: build and test (windows-latest)
🔇 Additional comments (15)
src/storage/src/redis_sets.rs (5)

25-25: LGTM! Clean import refactoring.

The removal of WriteBatch from the imports is correct and aligns with the batch abstraction introduced in this PR.


151-168: LGTM! Atomic batch operations correctly implemented.

The refactoring properly uses the batch abstraction with a single atomic commit for all metadata and member updates.


472-523: LGTM! Proper batch accumulation pattern.

The code correctly collects keys to delete, then applies all deletions and metadata updates in a single atomic batch commit.


635-659: LGTM! Atomic batch operations for member removal.

The batch correctly accumulates all member deletions and metadata updates before committing atomically.


795-837: LGTM! Atomic cross-set move operation.

The batch correctly ensures atomicity for the move operation across both source and destination sets with a single commit.

src/storage/src/redis_lists.rs (2)

105-108: push_core: delete/put ordering and meta update in the same commit looks right.

Collecting deletes then puts, and finally writing meta in the same batch should preserve the intended atomicity for list mutations (especially when reorganizing and bumping version).

Also applies to: 125-126, 156-157, 172-173, 184-185, 208-209, 224-225, 236-246


1032-1035: linsert: version bump + delete old version keys + write new version keys is a solid pattern.

This avoids in-place shifting complexity and keeps atomicity via a single batch commit.

Also applies to: 1041-1042, 1056-1057, 1063-1073

src/storage/src/redis_zsets.rs (3)

30-30: Import change only.


792-798: zrem: batch delete + meta update in one commit is correct.

Nice that meta removal when count hits zero is handled in the same batch.

Also applies to: 824-877


1473-1479: Range removals: batched member+score deletions + meta update are consistent.

Pattern is uniform and easy to reason about after the refactor.

Also applies to: 1547-1570, 1670-1693, 1712-1718, 1773-1796

src/storage/src/redis_hashes.rs (4)

23-23: Import change only.


86-122: hdel: collect-then-batch delete is solid.

Good: existence checks are done on the snapshot, then deletes + meta update are committed together via Batch.


637-665: hmset: good batching and correct “update meta only if new fields added” optimization.

data_to_write + conditional meta put keeps writes minimal while still committing atomically.

Also applies to: 694-711, 715-755, 758-759


779-800: hsetnx: batch path matches expected semantics.

The new-field path updates meta + data in one commit; existing field returns early without writes.

Also applies to: 832-844, 865-877, 883-885

src/storage/src/redis_strings.rs (1)

318-325: No issue detected. MetaCF is explicitly the default RocksDB column family (mapped to "default" in the CF name function). Reads via db.get_opt() and writes via batch.put(ColumnFamilyIndex::MetaCF, ...) both target the same CF, so there is no read/write inconsistency.

@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch from 0fd2c70 to a5b2f2f Compare January 13, 2026 08:00
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/storage/src/redis_lists.rs (1)

1080-1154: RPOPLPUSH is not atomic at the storage level (two separate batch commits).

Even though locks prevent concurrent interleavings, a crash between rpop_internal(...).commit() and push_core(...).commit() can lose the element or leave source/destination inconsistent. This is a core Redis guarantee for RPOPLPUSH / BRPOPLPUSH.

Consider a dedicated internal implementation that:

  • reads source tail element,
  • updates/deletes source element + source meta,
  • writes destination head element + destination meta,
  • and commits once with a single batch spanning both keys.
src/storage/src/redis_zsets.rs (2)

99-180: Fix incorrect float “same score” check in zadd (it’s currently wrong).

(existing_score, sm.score).1.abs() is just abs(sm.score), not the delta. This can skip updates incorrectly (especially for scores near 0).

Proposed fix
-                                if (existing_score, sm.score).1.abs() < f64::EPSILON {
+                                if (existing_score - sm.score).abs() < f64::EPSILON {
                                     // Score is the same, skip
                                     continue;
                                 } else {

1094-1278: ZINTERSTORE/ZUNIONSTORE destination update is not atomic (visible intermediate state).

zset_store_operation commits deletion of the destination, releases the lock, and later calls zadd (which commits separately). Other operations can observe “destination missing/empty” between these commits, which violates Redis atomicity expectations for these commands.

Suggestion: keep the destination lock through the full operation and write the final destination state in one commit (either by an internal no-lock zadd variant, or by assembling the final batch directly).

src/storage/src/redis_strings.rs (1)

1063-1155: Add multi-key locking to msetnx to prevent TOCTOU race condition.

Without acquiring locks on all keys before the existence check, another thread can insert a key between msetnx's check phase and write phase, violating the atomic "set none if any exists" semantics. Acquire and hold locks on all keys (sorted to avoid deadlocks) throughout the entire operation, matching the pattern already used for multi-key operations elsewhere in the codebase.

Proposed direction (sketch)
 pub fn msetnx(&self, kvs: &[(Vec<u8>, Vec<u8>)]) -> Result<bool> {
+    // Acquire locks for all keys (sorted) to prevent TOCTOU under concurrency.
+    let mut key_strs: Vec<String> = kvs
+        .iter()
+        .map(|(k, _)| String::from_utf8_lossy(k).to_string())
+        .collect();
+    key_strs.sort();
+    key_strs.dedup();
+    let _locks: Vec<ScopeRecordLock> = key_strs
+        .iter()
+        .map(|ks| ScopeRecordLock::new(self.lock_mgr.as_ref(), ks))
+        .collect();
+
     let db = self.db.as_ref().context(OptionNoneSnafu {
         message: "db is not initialized".to_string(),
     })?;
🤖 Fix all issues with AI agents
In @src/storage/src/redis_sets.rs:
- Around line 2315-2330: In clear_and_store_set, initial_meta_value() resets the
count to zero which causes stored metadata to have count=0; after creating
new_set_meta (ParsedSetsMetaValue) and before encoding/writing it (before
batch.put), call new_set_meta.modify_count(member_count) (or otherwise set the
count via ParsedSetsMetaValue::modify_count) so the correct member_count is
preserved instead of being overwritten by initial_meta_value().
🧹 Nitpick comments (5)
src/storage/src/redis_strings.rs (2)

318-325: Reduce repeated “single-put then commit” boilerplate (and keep semantics consistent).

This pattern is now repeated across many string write paths; consider a small helper on Redis like put_meta_and_commit(key, value) (or with_batch(|b| ...)) to centralize error context and avoid drift (e.g., future write options / metrics / batch-size guards).

Also applies to: 389-396, 581-588, 635-642, 698-705, 759-766, 831-838, 1212-1219, 1284-1291, 1429-1436, 1836-1843, 1856-1863, 1877-1884, 1938-1945, 1979-1986


2110-2151: Unbounded key collection + huge single batch in del_key / flush_db can be risky.

Both functions can accumulate very large keys_to_delete and then issue a single batch.commit(). This can spike memory and potentially exceed RocksDB WriteBatch practical limits. Consider:

  • streaming deletes directly into the batch while iterating (if borrow rules allow), and/or
  • chunking commits for flush_db (atomicity isn’t usually required there), and/or
  • a RocksDB-native approach (e.g., drop+recreate CF, delete_range, etc.) depending on your Engine API.

Also applies to: 2226-2261

src/storage/src/redis_hashes.rs (2)

86-122: Avoid extra allocations when collecting encoded keys for deletion.

encoded_key is already an owned Vec<u8>; keys_to_delete.push(encoded_key.to_vec()) clones again. Prefer pushing encoded_key directly (or store as Vec<Vec<u8>> and move it).


293-314: De-duplicate create_new_hash closures into a single helper.

The repeated closure blocks are easy to desync over time (meta encoding/versioning, batch wiring, etc.). A private fn create_hash_with_fields(&self, key, fields_iter) -> Result<()> would reduce risk and simplify future cluster-mode adjustments.

Also applies to: 637-665, 779-800, 904-926, 1060-1082

src/storage/src/redis_sets.rs (1)

2258-2287: Consider adding a comment about intentional type overwrite behavior.

The code at line 2259 checks if the existing destination is a Set type, but doesn't error if it's a different type—it simply skips collecting old members. This means a non-Set destination gets overwritten with a Set, which matches Redis semantics for SDIFFSTORE/SINTERSTORE/SUNIONSTORE.

A brief comment clarifying this intentional behavior would improve maintainability.

📝 Suggested documentation improvement
         // Delete existing destination set metadata and data
         let meta_val = db
             .get_cf(&cf_meta, &dest_base_meta_key)
             .context(RocksSnafu)?;
         if let Some(val) = meta_val {
+            // Only collect old members if destination is a valid Set.
+            // Per Redis semantics, *STORE commands overwrite any existing key type.
             if val.first().copied() == Some(DataType::Set as u8) {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd2c70 and a5b2f2f.

📒 Files selected for processing (9)
  • src/raft/src/network.rs
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/storage/src/lib.rs
  • src/raft/src/network.rs
  • src/storage/src/redis.rs
  • src/storage/src/batch.rs
🧰 Additional context used
🧬 Code graph analysis (2)
src/storage/src/redis_zsets.rs (1)
src/storage/src/batch.rs (2)
  • new (113-125)
  • new (221-223)
src/storage/src/redis_sets.rs (3)
src/storage/src/base_meta_value_format.rs (3)
  • new (56-63)
  • new (103-147)
  • encoded (234-236)
src/storage/src/member_data_key_format.rs (4)
  • new (42-50)
  • new (98-134)
  • key (136-138)
  • version (140-142)
src/storage/src/base_key_format.rs (3)
  • new (42-48)
  • new (69-73)
  • key (88-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test
  • GitHub Check: test-windows
  • GitHub Check: test-macos
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: cargo clippy (macos-latest)
  • GitHub Check: build and test (ubuntu-latest)
  • GitHub Check: build and test (windows-latest)
  • GitHub Check: cargo clippy (windows-latest)
🔇 Additional comments (9)
src/storage/src/redis_lists.rs (1)

105-246: Batching approach in push_core looks coherent (delete → put → meta → commit).

The collected deletes and puts applied in a single commit is aligned with the new Batch abstraction and avoids borrow conflicts.

src/storage/src/redis_strings.rs (1)

581-588: No action needed. Reads and writes are correctly aligned to the same column family.

The Engine::get_opt() method reads from RocksDB's default column family (when no CF is explicitly specified), and writes via batch.put(ColumnFamilyIndex::MetaCF, ...) correctly target the same CF since MetaCF maps to RocksDB's "default" column family. The batch implementation properly converts the MetaCF index to the corresponding CF handle and commits via put_cf(). There is no read/write inconsistency.

src/storage/src/redis_sets.rs (7)

25-25: LGTM!

Import correctly updated to remove WriteBatch since batch operations now use the abstracted Batch trait via self.create_batch().


151-168: LGTM!

The batch abstraction is correctly applied: creates batch via factory method, accumulates metadata and member data puts, then commits atomically. Error handling properly propagates through ?.


497-519: LGTM!

Good implementation pattern: batch is created only when needed (removed_count > 0), keys are collected first then batch-deleted, and metadata update/deletion is handled conditionally. All operations are committed atomically.


635-658: LGTM!

Batch operations correctly follow the early return check for empty members_to_pop. Member deletions and metadata updates are accumulated and committed atomically.


795-836: LGTM!

Complex cross-set operation (source → destination) is handled correctly with proper lock ordering to prevent deadlocks. All operations—source member deletion, destination member addition, and both metadata updates—are accumulated in a single batch and committed atomically.


2177-2191: LGTM!

Clean separation of concerns: sdiffstore calculates the difference and delegates the atomic store operation to the clear_and_store_set helper.


2225-2229: Good documentation of atomicity guarantee.

The method documentation clearly explains the atomicity contract, and the implementation correctly uses a single batch with one commit point to fulfill this guarantee.

@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch from a5b2f2f to 6caf66c Compare January 13, 2026 08:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/storage/src/redis_lists.rs (2)

1095-1169: rpoplpush is no longer crash-atomic (two separate commits).

Even with locks preventing interleaving, a crash after rpop_internal commits but before push_core commits can lose the element (and violates Redis RPOPLPUSH atomicity expectations). If possible, build a single batch that updates both source and destination list data + metas and commit once.


105-246: Document or guard the left_index arithmetic to prevent u64 underflow.

The code performs unchecked subtraction on left_index (e.g., line 118: left_index - values.len() and modify_left_index in list_meta_value_format.rs line 289). While the starting value of INITIAL_LEFT_INDEX = i64::MAX provides ample room in practice, add either: (1) a code comment documenting the invariant that left_index will not decrease by more than i64::MAX elements over the list's lifetime, or (2) use checked_sub with a version bump when approaching a rebase threshold.

src/storage/src/redis_zsets.rs (2)

117-123: Critical bug: score “equality” check is wrong ((existing_score, sm.score).1.abs()).

This currently checks abs(sm.score) < EPSILON, not abs(existing_score - sm.score) < EPSILON, so it will skip updates only when the new score is near 0.0.

Proposed fix
-                                if (existing_score, sm.score).1.abs() < f64::EPSILON {
+                                if (existing_score - sm.score).abs() < f64::EPSILON {
                                     // Score is the same, skip
                                     continue;
                                 } else {

1215-1278: ZINTERSTORE/ZUNIONSTORE delete-then-zadd sequence is not crash-atomic.

zset_store_operation commits deletion of the destination, releases the lock, then calls zadd (another commit). A crash between them leaves destination empty. If Redis-like atomicity is desired, consider writing the computed destination content in a single batch (or using a “write new version then swap meta pointer” approach).

🤖 Fix all issues with AI agents
In @src/storage/src/redis_sets.rs:
- Around line 2305-2327: The code uses
ParsedSetsMetaValue::modify_count(member_count) after calling
new_set_meta.initial_meta_value(), but modify_count() applies a delta
(saturating_add) rather than setting the count directly; replace the call to
modify_count(member_count) with set_count(member_count) so the metadata count is
explicitly set after initialization (refer to ParsedSetsMetaValue,
initial_meta_value(), modify_count(), and set_count()).
🧹 Nitpick comments (6)
src/storage/src/batch.rs (1)

201-264: BinlogBatch: put/delete returning Ok(()) is easy to misuse in cluster mode.

Since cluster mode is “not implemented”, consider failing fast in put/delete too (not just commit) to avoid callers accumulating lots of in-memory “successful” operations before discovering the error at the end. If you intentionally want to allow building the log first, please add a loud doc comment on BinlogBatch and/or on Redis::create_batch() cluster branch.

src/storage/src/redis_strings.rs (3)

318-325: Good migration to create_batch(); consider a small helper to remove repetitive put+commit boilerplate.

There are many single-key write paths now doing create_batch() -> put(MetaCF, ...) -> commit(). A helper like self.meta_put(key, value) (or self.commit_single_put(cf, k, v)) would reduce duplication and keep error contexts consistent.

Also applies to: 389-396, 581-588, 635-642, 698-705, 759-766, 831-838, 1063-1079, 1138-1155, 1212-1219, 1284-1291, 1429-1436, 1836-1843, 1856-1863, 1877-1884, 1938-1945, 1979-1986


2110-2151: del_key / flush_db: collecting all keys before deleting can OOM or create oversized batches.

For large datasets, keys_to_delete can grow without bound and the resulting RocksDB WriteBatch can become huge. Consider chunking (e.g., commit every N deletions) while iterating.

Also applies to: 2226-2261


1106-1111: Redundant MetaCF handle fetch in msetnx.

let _cf = ...get_cf_handle(MetaCF)...?; is only used as an initialization check; batch.put(MetaCF, ...) should already fail if MetaCF is missing. If you keep it, consider a brief comment clarifying it’s intentional.

src/storage/src/redis_hashes.rs (1)

293-314: Code duplication: create_new_hash helper closures are duplicated across multiple functions.

The create_new_hash closure is defined nearly identically in hset, hmset, hsetnx, hincrby, and hincrbyfloat. Consider extracting this to a shared private method to reduce code duplication and improve maintainability.

♻️ Suggested refactor

Extract the common logic into a private helper method:

/// Helper to create a new hash with a single field
fn create_new_hash_single_field(
    &self,
    key: &[u8],
    base_meta_key: &[u8],
    field: &[u8],
    value: &[u8],
) -> Result<()> {
    let mut hashes_meta = HashesMetaValue::new(Bytes::copy_from_slice(&1u64.to_le_bytes()));
    hashes_meta.inner.data_type = DataType::Hash;
    let version = hashes_meta.update_version();

    let data_key = MemberDataKey::new(key, version, field);
    let data_value = BaseDataValue::new(value.to_vec());

    let mut batch = self.create_batch()?;
    batch.put(ColumnFamilyIndex::MetaCF, base_meta_key, &hashes_meta.encode())?;
    batch.put(ColumnFamilyIndex::HashesDataCF, &data_key.encode()?, &data_value.encode())?;
    batch.commit()
}
src/storage/src/redis_sets.rs (1)

2062-2066: Minor: Unused variable _cf_data.

The variable _cf_data is retrieved but never used in sinter. Since sismember is called which retrieves its own handles, this lookup is unnecessary.

♻️ Suggested fix
-        let _cf_data =
-            self.get_cf_handle(ColumnFamilyIndex::SetsDataCF)
-                .context(OptionNoneSnafu {
-                    message: "cf data is not initialized".to_string(),
-                })?;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5b2f2f and 6caf66c.

📒 Files selected for processing (10)
  • src/raft/src/network.rs
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
  • src/storage/tests/redis_list_test.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/raft/src/network.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/storage/src/redis_strings.rs (2)
src/storage/src/redis.rs (1)
  • new (115-149)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/tests/redis_list_test.rs (1)
src/storage/src/raft_integration.rs (1)
  • create_test_redis (100-112)
src/storage/src/batch.rs (1)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/redis_sets.rs (4)
src/storage/src/base_meta_value_format.rs (3)
  • new (56-63)
  • new (103-147)
  • encoded (234-236)
src/storage/src/member_data_key_format.rs (4)
  • new (42-50)
  • new (98-134)
  • key (136-138)
  • version (140-142)
src/storage/src/base_key_format.rs (3)
  • new (42-48)
  • new (69-73)
  • key (88-90)
src/storage/src/base_data_value_format.rs (2)
  • new (40-47)
  • new (75-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test-windows
  • GitHub Check: test
  • GitHub Check: test-macos
  • GitHub Check: cargo clippy (macos-latest)
  • GitHub Check: cargo clippy (windows-latest)
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: build and test (windows-latest)
  • GitHub Check: build and test (ubuntu-latest)
🔇 Additional comments (22)
src/storage/src/batch.rs (2)

266-282: Tests cover the “commit must error” guardrail—good.

This prevents silent data loss if BinlogBatch is accidentally used.


56-90: Batch: Send is appropriate; Sync is not needed.

Batches are always created as local mutable variables within a single function scope and moved into commit(self: Box<Self>), consuming them. They are never shared across threads, stored in Arc, or accessed from multiple thread contexts. The current Send bound is sufficient.

Likely an incorrect or invalid review comment.

src/storage/tests/redis_list_test.rs (1)

598-617: LPUSHX/RPUSHX expectations now match Redis semantics (non-existent key => 0, no creation).

These test updates look consistent with Redis behavior and align with the new “delete meta when empty” list implementation.

Also applies to: 625-645

src/storage/src/redis_lists.rs (2)

317-354: Deleting list metadata when empty is the right Redis-compatible behavior.

This directly supports the updated LPUSHX/RPUSHX semantics and makes EXISTS behave as expected for empty lists.

Also applies to: 393-430, 469-506


733-823: Batching across complex list mutations looks consistent (delete first, then put, then meta).

The pattern of collecting keys, applying deletions before writes, and committing once is a good fit for RocksDB atomic batches.

Also applies to: 902-952, 1047-1088

src/storage/src/redis_zsets.rs (1)

101-180: Batch migration for ZSET writes is clean and consistent.

Using a single batch for “delete old score key + write new member/score + meta update” is the right approach for consistency between ZsetsDataCF and ZsetsScoreCF.

Also applies to: 188-214, 383-410, 824-877, 1547-1575, 1670-1698, 1773-1801

src/storage/src/redis_hashes.rs (9)

23-23: Import cleanup looks correct.

The WriteBatch import was removed since direct WriteBatch usage is replaced with the batch abstraction.


86-121: Batch abstraction correctly applied for hdel.

The refactoring properly:

  1. Collects keys to delete first
  2. Creates a single batch for all operations
  3. Commits atomically with both data deletions and metadata update

This ensures atomicity for the delete operation.


346-357: Batch operations correctly implemented for stale/empty hash case.

The batch correctly groups the metadata update and data write into a single atomic commit.


376-382: Batch correctly used for value update case.

Single field update is properly batched and committed.


396-407: Batch correctly used for new field insertion.

Both metadata count update and data insertion are properly grouped in a single batch commit.


648-664: hmset helper correctly batches multiple field writes.

The helper properly creates a batch, writes metadata, then iterates through all field-value pairs adding them to the batch before a single commit.


715-754: Efficient batching pattern for hmset with existing hash.

The implementation correctly:

  1. Collects all data to write first (line 716)
  2. Checks field existence to track new fields count
  3. Creates a single batch for all writes
  4. Only updates metadata if new fields were added (line 747-752)

This ensures minimal overhead and atomic writes.


1000-1006: Batch correctly applied for hincrby existing value update.

Single atomic commit for the updated value.


1159-1165: Batch correctly applied for hincrbyfloat existing value update.

Consistent with the hincrby pattern.

src/storage/src/redis_sets.rs (7)

25-25: Import correctly updated.

WriteBatch removed from imports since the batch abstraction is now used instead.


151-168: Batch abstraction correctly applied for sadd.

The implementation properly batches metadata and all member additions before a single commit, ensuring atomicity.


472-519: Batch abstraction correctly applied for srem.

The refactoring properly:

  1. Collects keys to delete (line 472)
  2. Creates batch and deletes all collected keys
  3. Conditionally deletes or updates metadata
  4. Commits atomically

This ensures the remove operation is atomic.


635-658: Batch abstraction correctly applied for spop.

All member deletions and metadata updates are properly grouped in a single batch commit.


795-836: Batch abstraction correctly applied for smove.

The implementation properly handles:

  1. Deleting from source set
  2. Updating source metadata (delete if empty, update count otherwise)
  3. Adding to destination set (if not already present)
  4. Updating destination metadata

All operations are in a single atomic batch commit.


2189-2190: Good refactoring to use shared helper.

Using clear_and_store_set eliminates code duplication across sdiffstore, sinterstore, and sunionstore.


2225-2349: Well-designed atomic helper method for set store operations.

The clear_and_store_set method correctly:

  1. Locks the destination key
  2. Collects existing members to delete
  3. Creates a single batch for all operations
  4. Deletes old data keys
  5. Handles empty result case (deletes metadata, commits, returns)
  6. Deduplicates new members
  7. Creates new metadata with correct count
  8. Writes all new members
  9. Commits atomically

This ensures no intermediate states are visible.

arana-db#222)

This commit introduces a Batch trait to abstract batch write operations,
enabling support for both standalone (RocksDB) and future cluster (Raft) modes.

Changes:
- Add batch.rs module with Batch trait, RocksBatch, and BinlogBatch implementations
- Add create_batch() method to Redis struct for creating batch instances
- Refactor redis_strings.rs to use Batch trait (2 places)
- Refactor redis_hashes.rs to use Batch trait (6 places)
- Refactor redis_lists.rs to use Batch trait (7 places)
- Refactor redis_sets.rs to use Batch trait (6 places)
- Refactor redis_zsets.rs to use Batch trait (8 places)
- Add explicit error handling for invalid column family index

This abstraction allows seamless switching between direct RocksDB writes
and Raft consensus-based writes in the future.
@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch from 6caf66c to 7560057 Compare January 13, 2026 08:51
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/storage/src/redis_zsets.rs (2)

99-180: Fix score equality check in zadd—current code compares the wrong value.
This can incorrectly treat scores as “equal” whenever sm.score is near 0, even if existing_score differs.

Proposed fix
-                                if (existing_score, sm.score).1.abs() < f64::EPSILON {
+                                if (existing_score - sm.score).abs() < f64::EPSILON {
                                     // Score is the same, skip
                                     continue;
                                 } else {

1234-1265: Clear and re-add are two separate lock acquisitions with an interleaving window.

The code clears the destination (batch deletes + commit) while holding a lock, then releases the lock before calling zadd(). This creates a brief window where other operations can access an incomplete state of destination. If this operation requires atomicity (e.g., atomically replacing all members), consider extending the single lock scope to encompass both the clear and the subsequent zadd() call.

src/storage/src/redis_strings.rs (2)

2110-2151: del_key prefix-scan likely won’t delete composite-type data keys (prefix mismatch).
You use BaseKey::encode() (includes suffix reserve) as the prefix to scan data CFs, but formats like MemberDataKey don’t start with the full BaseKey encoding. This means keys_to_delete will usually stay empty for Sets/Hashes/Lists/ZSets data CFs, leaving the comment “works for all data types” inaccurate.

Suggested direction
-                    // Prefix-scan data CF and delete all derived keys
-                    let iter = db.iterator_cf(
-                        &cf,
-                        rocksdb::IteratorMode::From(&encoded, rocksdb::Direction::Forward),
-                    );
+                    // TODO: Use a key-format-appropriate prefix for each CF (do NOT use BaseKey::encode()).
+                    // For versioned member keys, prefer building prefixes from the meta's version
+                    // (e.g. MemberDataKey::new(key, version, &[]).encode_seek_key()).
+                    let iter = db.iterator_cf(
+                        &cf,
+                        rocksdb::IteratorMode::From(&encoded, rocksdb::Direction::Forward),
+                    );

2226-2261: Consider chunking deletes to avoid O(total_keys) memory accumulation in flush_db().

The current implementation collects all keys from all 6 column families into a single Vec before batch deleting, which would load the entire dataset into memory. For large databases, refactor to periodically commit batches (e.g., every N keys) using the existing batch.clear() method to reset operations between commits.

CF drop/recreate or range-delete optimizations are not exposed by the current Engine abstraction, so chunking is the viable approach here.

src/storage/src/redis_lists.rs (1)

720-722: Inconsistent with batch abstraction - lset bypasses the batch layer.

This direct db.put_cf() call is inconsistent with the batch abstraction pattern used throughout this PR. In cluster mode, this write would bypass the Raft consensus layer since it doesn't go through BinlogBatch.

🐛 Proposed fix
-        // Update the element
-        db.put_cf(lists_data_cf, &encoded_data_key, &encoded_data_value)
-            .context(RocksSnafu)?;
+        // Update the element using batch for consistency with cluster mode
+        let mut batch = self.create_batch()?;
+        batch.put(ColumnFamilyIndex::ListsDataCF, &encoded_data_key, &encoded_data_value)?;
+        batch.commit()?;
🤖 Fix all issues with AI agents
In @src/storage/src/redis.rs:
- Around line 320-360: The create_batch() implementation currently builds
cf_handles by calling get_cf_handle(ColumnFamilyIndex::...) which uses enum
indices and can return wrong handles if self.handles was filtered; change it to
resolve column families by name via the DB API instead: call
db.cf_handle(ColumnFamilyIndex::MetaCF.name()) (and the same for HashesDataCF,
SetsDataCF, ListsDataCF, ZsetsDataCF, ZsetsScoreCF), wrap each Option into the
Arc/BoundColumnFamily type expected by RocksBatch, and pass that Vec to
crate::batch::RocksBatch::new; ensure you remove reliance on self.handles or
index-based lookup and keep the same error handling around
self.db.as_ref().context(...).
🧹 Nitpick comments (3)
src/storage/src/redis_hashes.rs (1)

293-314: Consider extracting duplicate create_new_hash closures into a shared helper method.

There are 5 nearly identical create_new_hash closures across hset, hmset, hsetnx, hincrby, and hincrbyfloat. Each follows the same pattern:

  1. Create HashesMetaValue with initial count
  2. Update version
  3. Create data key/value
  4. Batch put meta + data
  5. Commit

This duplication increases maintenance burden. Consider extracting to a private helper method on Redis.

♻️ Suggested approach
impl Redis {
    /// Helper to create a new hash with a single field
    fn create_new_hash_single_field(
        &self,
        base_meta_key: &[u8],
        key: &[u8],
        field: &[u8],
        value: &[u8],
    ) -> Result<()> {
        let mut hashes_meta = HashesMetaValue::new(Bytes::copy_from_slice(&1u64.to_le_bytes()));
        hashes_meta.inner.data_type = DataType::Hash;
        let version = hashes_meta.update_version();

        let data_key = MemberDataKey::new(key, version, field);
        let data_value = BaseDataValue::new(value.to_vec());

        let mut batch = self.create_batch()?;
        batch.put(ColumnFamilyIndex::MetaCF, base_meta_key, &hashes_meta.encode())?;
        batch.put(ColumnFamilyIndex::HashesDataCF, &data_key.encode()?, &data_value.encode())?;
        batch.commit()
    }
}

Also applies to: 637-665, 779-800, 904-926, 1060-1082

src/storage/src/batch.rs (2)

95-98: Consider deriving EXPECTED_CF_COUNT from the enum to avoid manual sync.

The hardcoded constant relies on developers remembering to update it when adding new column families. While cf_index_to_usize's exhaustive match provides compile-time safety for the mapping, the count itself could get out of sync.

♻️ Alternative approach using a const fn

If ColumnFamilyIndex derives common traits, you could potentially use:

// In the enum definition file
impl ColumnFamilyIndex {
    pub const COUNT: usize = 6; // Or use a macro to count variants
}

Then reference ColumnFamilyIndex::COUNT instead of a separate constant. This keeps the count co-located with the enum definition, making it more likely to be updated together.

Also applies to: 130-137


258-280: BinlogBatch silently accepts operations but fails on commit - consider failing early.

Currently put and delete return Ok(()) but commit fails. This could lead to confusing debugging scenarios where many operations "succeed" before finally failing at commit.

Consider either:

  1. Return an error immediately on put/delete to fail fast
  2. Add a warning log when operations are added to an unimplemented batch
♻️ Option 1: Fail early on put/delete
 impl Batch for BinlogBatch {
     fn put(&mut self, _cf_idx: ColumnFamilyIndex, _key: &[u8], _value: &[u8]) -> Result<()> {
-        // TODO: Implement when Raft integration is ready
-        // Create binlog entry and add to entries
-        self.count += 1;
-        Ok(())
+        BatchSnafu {
+            message: "BinlogBatch is not implemented - Raft integration pending".to_string(),
+        }
+        .fail()
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6caf66c and 7560057.

📒 Files selected for processing (10)
  • src/raft/src/network.rs
  • src/storage/src/batch.rs
  • src/storage/src/lib.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_hashes.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
  • src/storage/tests/redis_list_test.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/raft/src/network.rs
  • src/storage/tests/redis_list_test.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/storage/src/redis.rs (1)
src/storage/src/batch.rs (1)
  • get_cf_handle (177-194)
src/storage/src/redis_sets.rs (2)
src/storage/src/member_data_key_format.rs (4)
  • new (42-50)
  • new (98-134)
  • key (136-138)
  • version (140-142)
src/storage/src/base_key_format.rs (3)
  • new (42-48)
  • new (69-73)
  • key (88-90)
src/storage/src/redis_lists.rs (5)
src/cmd/src/list.rs (14)
  • new (38-48)
  • new (83-93)
  • new (128-138)
  • new (202-212)
  • new (278-288)
  • new (322-332)
  • new (380-390)
  • new (449-459)
  • new (513-523)
  • new (578-588)
  • new (634-644)
  • new (679-689)
  • new (724-734)
  • new (782-792)
src/storage/src/storage.rs (1)
  • new (79-82)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/storage_impl.rs (1)
  • key (150-150)
src/storage/src/base_key_format.rs (1)
  • key (88-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build and test (ubuntu-latest)
  • GitHub Check: cargo clippy (macos-latest)
  • GitHub Check: cargo clippy (windows-latest)
  • GitHub Check: build and test (windows-latest)
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: test-windows
  • GitHub Check: test-macos
  • GitHub Check: test
🔇 Additional comments (18)
src/storage/src/lib.rs (1)

42-43: Public re-export looks consistent with the feature goal; just be aware it’s a semver commitment.
If you want more flexibility later, consider re-exporting only Batch and keeping concrete impls behind a feature flag.

Also applies to: 69-70

src/storage/src/redis_zsets.rs (5)

30-31: Import change is fine.


188-213: Batch-based create path for new zset looks consistent.


321-410: Batch-based score update in zincrby looks good (incl. NaN/Inf guard).


792-877: zrem now commits all deletes + meta update atomically—good.


1473-1574: Range-removal paths now batch deletes + meta update—nice consistency.

Also applies to: 1670-1697, 1712-1801

src/storage/src/redis_strings.rs (1)

318-325: Batch migration for single-key string mutations is consistent and readable.

Also applies to: 389-396, 581-588, 635-642, 698-705, 759-766, 831-838, 1212-1219, 1284-1291, 1429-1436, 1836-1843, 1856-1863, 1877-1884, 1938-1945, 1979-1986

src/storage/src/redis_sets.rs (3)

25-27: Import change is fine.


151-169: Batch usage in core mutation ops (sadd/srem/spop/smove) looks consistent and improves atomicity.

Also applies to: 498-520, 635-659, 795-837


2189-2191: The concern about improper destination clearing is not substantiated by the current implementation.

The clear_and_store_set() function properly handles all destination key types:

  1. String destinations: When destination holds a String, the metadata is read from MetaCF. Since the type check at line 2259 identifies it as non-Set, the member deletion block is correctly skipped (Strings have no members in SetsDataCF to delete). The old String metadata is then overwritten atomically at lines 2305-2307 with new Set metadata using the same key encoding.

  2. Set destinations: For existing Sets, old member keys are deleted from SetsDataCF (lines 2264-2290), and the metadata is updated with a new version.

  3. Key storage model: Both String values and Set metadata use the same MetaCF column family with BaseKey/BaseMetaKey encoding. Each key has a single metadata entry; there are no separate "old key(s)" left behind when overwriting a String with a Set—the entry is simply replaced atomically.

The current implementation properly maintains overwrite semantics and prevents stale data through proper version management and atomic batch operations. No type validation or garbage data issues exist in the code.

src/storage/src/redis_hashes.rs (2)

86-121: LGTM - batch operations for hdel are correctly implemented.

The refactoring properly collects keys to delete first, then creates the batch and applies all operations atomically. The count update and meta value update are correctly bundled in the same batch.


346-357: LGTM - consistent batch operations across all hash mutation functions.

All batch operations correctly:

  • Create batch via self.create_batch()?
  • Bundle meta and data updates in the same batch for atomicity
  • Propagate errors via ? operator
  • Call batch.commit()? at the end

Also applies to: 376-407, 693-754, 832-876, 959-1032, 1115-1191

src/storage/src/redis_lists.rs (3)

105-245: LGTM - push_core correctly implements batch operations.

The implementation properly:

  1. Collects puts and deletes in vectors
  2. Applies deletes before puts (correct order for version transitions)
  3. Includes metadata update in the same batch
  4. Handles both empty and non-empty list cases correctly

341-353: LGTM - pop operations correctly implement batch with metadata cleanup.

All pop operations correctly:

  • Delete data keys in a single batch
  • Delete metadata when list becomes empty (Redis-compatible behavior)
  • Update metadata otherwise
  • Use batch.commit() for atomic execution

Also applies to: 417-429, 493-505


769-820: LGTM - ltrim, lrem, and linsert correctly implement batch operations.

The implementations properly handle:

  • Conditional metadata deletion/update via flags
  • Collection of puts and deletes before batch creation
  • Atomic commit of all operations

Also applies to: 903-951, 1047-1087

src/storage/src/batch.rs (3)

56-90: LGTM - well-designed Batch trait with proper documentation.

The trait definition is clean with:

  • Clear method signatures for put, delete, commit, count, clear
  • Good documentation explaining error handling approach
  • Send bound enables cross-thread usage
  • commit(self: Box<Self>) properly consumes the batch to prevent reuse after commit

196-225: LGTM - RocksBatch implementation is correct and well-structured.

The implementation:

  • Properly validates CF handles before use via get_cf_handle
  • Correctly increments count on put/delete operations
  • Uses context(RocksSnafu) for proper error propagation on commit
  • clear() resets both the inner WriteBatch and count

292-308: LGTM - basic tests cover BinlogBatch placeholder behavior.

The tests verify:

  • Default initialization with count = 0
  • Commit correctly returns an error for the unimplemented batch

Additional tests for RocksBatch would require mocking the Engine trait, which may be covered in integration tests.

@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch from 0c274a4 to f1e29c6 Compare January 13, 2026 09:24
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/storage/src/redis_lists.rs (1)

657-727: Fix unused variable lists_data_cf to resolve pipeline failure.

The variable is obtained via the macro but never used since lset now uses the batch abstraction.

🔧 Proposed fix
-        let (db, cfs) = get_db_and_cfs!(self, ColumnFamilyIndex::ListsDataCF);
-        let lists_data_cf = &cfs[0];
+        let (db, _cfs) = get_db_and_cfs!(self, ColumnFamilyIndex::ListsDataCF);

Or if the macro is only needed for db:

-        let (db, cfs) = get_db_and_cfs!(self, ColumnFamilyIndex::ListsDataCF);
-        let lists_data_cf = &cfs[0];
+        let (db, _) = get_db_and_cfs!(self, ColumnFamilyIndex::ListsDataCF);
🤖 Fix all issues with AI agents
In @src/storage/src/redis_zsets.rs:
- Around line 1096-1102: The doc comment above the internal helper for
ZINTERSTORE and ZUNIONSTORE has a mis-indented list item causing a lint error;
adjust the doc comment so the numbered list items ("1. Collects members...", "2.
Deletes any existing destination data", "3. Writes new result data") are
indented as part of the doc paragraph (indent each list line by four spaces or
align them under the paragraph text) so the Rust doc linter recognizes them as a
list.
🧹 Nitpick comments (2)
src/storage/src/batch.rs (1)

287-303: Unit tests provide basic coverage for BinlogBatch.

Consider adding tests for RocksBatch when integration test infrastructure is available, particularly for error cases like invalid CF handles.

src/storage/src/redis_strings.rs (1)

1106-1110: Unused variable _cf should be removed.

The _cf variable is obtained but never used since the batch abstraction handles CF selection internally.

♻️ Proposed fix
-        let _cf = self
-            .get_cf_handle(ColumnFamilyIndex::MetaCF)
-            .context(OptionNoneSnafu {
-                message: "cf is not initialized".to_string(),
-            })?;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7560057 and f1e29c6.

📒 Files selected for processing (5)
  • src/storage/src/batch.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/storage/src/redis.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/storage/src/redis_strings.rs (2)
src/storage/src/batch.rs (2)
  • new (117-141)
  • new (242-244)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
src/storage/src/redis_lists.rs (1)
src/storage/src/batch.rs (2)
  • new (117-141)
  • new (242-244)
src/storage/src/redis_zsets.rs (1)
src/storage/src/batch.rs (3)
  • new (117-141)
  • new (242-244)
  • default (248-250)
src/storage/src/batch.rs (2)
src/storage/src/redis.rs (1)
  • get_cf_handle (313-323)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
🪛 GitHub Actions: ci
src/storage/src/redis_lists.rs

[error] 664-664: unused variable: lists_data_cf during 'make lint' (rustc error) - Command failed with exit code 1.

src/storage/src/redis_zsets.rs

[error] 1291-1291: Formatting check failed: rustfmt suggested change at line 1291. Run 'make fmt-check' again after applying formatting changes (or run 'cargo fmt' / 'make fmt' to fix).

🪛 GitHub Actions: Tests
src/storage/src/redis_lists.rs

[error] 664-664: unused variable: lists_data_cf

🪛 GitHub Check: cargo clippy (ubuntu-latest)
src/storage/src/redis_zsets.rs

[failure] 1102-1102:
doc list item without indentation

🪛 GitHub Check: cargo fmt (ubuntu-latest)
src/storage/src/redis_zsets.rs

[warning] 1291-1291:
Diff in /home/runner/work/kiwi/kiwi/src/storage/src/redis_zsets.rs

🪛 GitHub Check: lint
src/storage/src/redis_zsets.rs

[failure] 1102-1102:
doc list item without indentation

🔇 Additional comments (24)
src/storage/src/batch.rs (5)

56-90: Well-designed trait abstraction for batch operations.

The Batch trait properly abstracts the batch write mechanism with clear documentation. The Send bound enables cross-thread usage, and returning Result<()> instead of panicking is appropriate for production storage systems.


106-142: Constructor validation is appropriate but consider using a debug assertion.

The assert_eq! will panic in production if there's a mismatch. This is documented as a programming error, but consider whether a fallible constructor returning Result might be safer for library consumers.

That said, since this is an internal invariant that should never be violated at runtime (it would indicate a code bug), the current approach is reasonable.


172-189: Good error handling for missing CF handles.

The get_cf_handle function properly returns a descriptive error with the CF index information, which will help debugging initialization issues.


191-220: RocksBatch implementation is correct and straightforward.

The implementation properly delegates to RocksDB's WriteBatch and tracks count internally. The commit method correctly uses the Box<Self> pattern to consume the batch.


253-285: BinlogBatch placeholder correctly prevents silent data loss.

The commit method returning an error rather than silently succeeding is the right approach for an unimplemented feature. This ensures callers will be aware that cluster mode isn't ready.

src/storage/src/redis_strings.rs (6)

318-324: Correct batch-based write for setrange.

The migration to create_batch() with ColumnFamilyIndex::MetaCF follows the new pattern correctly.


389-395: Correct batch-based write for append.

Consistent with the new batch abstraction pattern.


581-587: Correct batch-based write for set.

Simple and correct migration.


1063-1078: mset correctly uses batch for atomic multi-key operation.

The batch is created once and all key-value pairs are added before a single commit, ensuring atomicity.


2083-2162: del_key correctly collects keys before batch deletion to avoid iterator invalidation.

The approach of collecting keys to delete first, then performing batch deletions, is correct and avoids modifying the iterator while iterating. The prefix-based scan for data CFs properly handles composite data types.


2233-2285: flush_db chunked deletion is well-designed for large databases.

The 1000-key chunk size is reasonable. Creating a new batch per chunk ensures memory is released between iterations. This prevents OOM issues on large databases.

src/storage/src/redis_zsets.rs (6)

30-33: Unused import removed appropriately.

The WriteBatch import was removed as it's no longer directly used, replaced by the batch abstraction.


117-144: Good floating-point score comparison using epsilon.

Using (existing_score - sm.score).abs() < f64::EPSILON is the correct approach for comparing floating-point numbers for equality.


99-181: zadd correctly updates both data and score CFs atomically.

The batch properly handles member updates across ZsetsDataCF and ZsetsScoreCF, including deleting old score keys when scores change.


1222-1313: zset_store_operation atomicity is correctly implemented.

The function properly:

  1. Collects members from source zsets
  2. Acquires lock on destination
  3. Deletes existing destination data in the same batch
  4. Writes new result data
  5. Commits all in one batch

This ensures atomic replacement of destination zset.


826-878: zrem correctly handles empty zset cleanup.

When all members are removed, the meta key is deleted. Otherwise, the count is updated. This is consistent with Redis behavior.


1286-1291: No formatting issue detected at lines 1286-1291.

The code at these lines is properly formatted with consistent indentation and correct Rust conventions. Line 1291 is blank, and the batch.put() call (lines 1286-1290) follows standard formatting patterns. The formatting does not require changes.

src/storage/src/redis_lists.rs (7)

105-107: Good refactoring to collect batch operations before commit.

Using vectors to collect puts and deletes before applying them in a single batch is a clean pattern that improves readability and ensures atomicity.


236-245: Batch commit correctly applies deletes before puts.

The order of operations (deletes first, then puts) is appropriate for reorganizing list elements when pushing to the head.


341-353: Correct empty list cleanup in lpop.

Deleting the meta key when the list becomes empty ensures EXISTS returns false and LPUSHX/RPUSHX behave correctly per Redis protocol.


720-727: lset correctly uses batch for consistency with cluster mode.

Even for single-element updates, using the batch abstraction ensures future cluster-mode compatibility.


774-776: Good use of boolean flags to track batch operations.

Using delete_meta and update_meta flags to determine which operations to include in the batch is clear and maintainable.


814-825: ltrim batch commit handles both delete and update cases correctly.

The conditional logic properly handles the case where the entire list is deleted vs. when it's just trimmed.


1100-1173: rpoplpush atomicity is maintained through proper locking.

The implementation correctly:

  1. Handles same-key case with single lock
  2. Acquires locks in sorted order for different keys to prevent deadlocks
  3. Uses internal methods to avoid double locking

- Fix zadd score equality check bug (comparing difference instead of
  just sm.score)
- Fix lset to use batch layer instead of direct db.put_cf()
- Fix zunionstore/zinterstore atomicity by merging delete and write
  into single batch commit
- Improve EXPECTED_CF_COUNT by deriving from ColumnFamilyIndex::COUNT
- Fix del_key prefix scan to use correct data key prefix format
- Optimize flush_db with chunked deletion (1000 keys per batch) to
  avoid memory issues with large databases
@Tsukikage7 Tsukikage7 force-pushed the feature/support-batch branch from f1e29c6 to f76ba07 Compare January 13, 2026 09:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/storage/src/batch.rs (1)

144-189: Consider deriving index from enum definition to reduce maintenance burden.

The explicit match in cf_index_to_usize ensures compile-time exhaustiveness, which is good. However, the hardcoded indices (0-5) must stay in sync with how column family handles are populated in Redis::create_batch(). If the order changes, this could cause subtle bugs.

💡 Alternative: Derive index from enum

If ColumnFamilyIndex derives a trait like num_enum::IntoPrimitive or has explicit discriminants, you could use cf_idx as usize directly. This would eliminate the need to manually maintain the mapping:

// If ColumnFamilyIndex has explicit discriminants:
#[repr(usize)]
pub enum ColumnFamilyIndex {
    MetaCF = 0,
    HashesDataCF = 1,
    // ...
}

#[inline]
fn cf_index_to_usize(cf_idx: ColumnFamilyIndex) -> usize {
    cf_idx as usize
}
src/storage/src/redis_strings.rs (1)

1106-1110: Remove unused _cf variable.

The _cf variable is fetched but never used. This appears to be dead code from before the refactoring.

🧹 Remove unused variable
     pub fn msetnx(&self, kvs: &[(Vec<u8>, Vec<u8>)]) -> Result<bool> {
         let db = self.db.as_ref().context(OptionNoneSnafu {
             message: "db is not initialized".to_string(),
         })?;

-        let _cf = self
-            .get_cf_handle(ColumnFamilyIndex::MetaCF)
-            .context(OptionNoneSnafu {
-                message: "cf is not initialized".to_string(),
-            })?;
-
         // Check if any key exists and is not expired
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1e29c6 and f76ba07.

📒 Files selected for processing (5)
  • src/storage/src/batch.rs
  • src/storage/src/redis.rs
  • src/storage/src/redis_lists.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/storage/src/redis.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/storage/src/redis_lists.rs (2)
src/storage/src/batch.rs (2)
  • new (117-141)
  • new (242-244)
src/storage/src/redis.rs (1)
  • new (120-154)
src/storage/src/redis_zsets.rs (1)
src/storage/src/batch.rs (3)
  • new (117-141)
  • new (242-244)
  • default (248-250)
src/storage/src/redis_strings.rs (2)
src/storage/src/batch.rs (2)
  • new (117-141)
  • new (242-244)
src/storage/src/storage_define.rs (1)
  • encode_user_key (49-68)
src/storage/src/batch.rs (2)
src/storage/src/redis.rs (1)
  • get_cf_handle (313-323)
src/engine/src/rocksdb_engine.rs (1)
  • db (38-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test-macos
  • GitHub Check: test-windows
  • GitHub Check: test
  • GitHub Check: build and test (macos-latest)
  • GitHub Check: cargo clippy (macos-latest)
  • GitHub Check: cargo clippy (windows-latest)
  • GitHub Check: build and test (ubuntu-latest)
  • GitHub Check: build and test (windows-latest)
🔇 Additional comments (34)
src/storage/src/batch.rs (6)

1-46: LGTM! Well-structured module header and imports.

The module documentation clearly explains the design intent for standalone vs. cluster modes, and the usage example is helpful. The imports are minimal and appropriate.


47-90: LGTM! Well-designed trait with appropriate ownership semantics.

The commit(self: Box<Self>) signature correctly consumes the batch to prevent reuse after commit. The Send bound enables usage in async contexts. Error handling via Result<()> is consistent across all operations.


191-220: LGTM! Clean Batch implementation for RocksBatch.

The implementation correctly:

  • Validates CF handles before operations and propagates errors
  • Tracks operation count only after successful operations
  • Delegates commit to the underlying engine with proper error context
  • Resets both internal state and count on clear

222-285: LGTM! BinlogBatch placeholder correctly fails on commit.

The design appropriately prevents silent data loss by returning an error on commit(). The put/delete operations succeed (incrementing count) but the final commit will fail - this is acceptable for a placeholder since:

  1. It allows code paths to be tested without cluster mode
  2. It fails loudly at commit time rather than silently losing data

The TODO comments clearly document what needs to be implemented for Raft integration.


287-303: LGTM! Basic test coverage for BinlogBatch.

The tests verify the essential behavior: default state and commit failure. Additional tests for put/delete incrementing count could be added, but the current coverage is sufficient for the placeholder implementation.


92-142: LGTM! RocksBatch construction with appropriate validation.

The assert_eq for cf_handles.len() vs ColumnFamilyIndex::COUNT is a good defensive programming practice to catch configuration mismatches early. The panic is acceptable here since it indicates a programming error that should be caught during development/testing. ColumnFamilyIndex::COUNT is correctly defined as 6, matching the 6 column family variants (MetaCF, HashesDataCF, SetsDataCF, ListsDataCF, ZsetsDataCF, ZsetsScoreCF).

src/storage/src/redis_zsets.rs (9)

30-30: LGTM! Import cleanup reflects the batch abstraction migration.

Removing WriteBatch and WriteOptions imports is correct since batch operations are now abstracted through the Batch trait.


99-181: LGTM! zadd correctly uses batch abstraction for atomic updates.

The refactoring properly:

  • Creates batch via self.create_batch()?
  • Uses ColumnFamilyIndex enums for type-safe CF access
  • Handles both existing zset updates and new zset creation
  • Commits batch atomically at the end of each code path

383-411: LGTM! zincrby correctly batches score update operations.

The batch properly groups the old score key deletion with new score/member key insertions for atomic update. The delegation to zadd() for new members is appropriate since zadd handles its own batch.


826-878: LGTM! zrem correctly batches member deletions and meta updates.

The implementation properly:

  • Batches all score and member key deletions
  • Handles the empty zset case by deleting the meta key
  • Updates meta count when members remain
  • Commits atomically

1096-1103: LGTM! Well-documented atomic batch operation.

The updated docstring clearly explains the three-phase operation (collect, delete, write) and emphasizes the atomicity guarantee provided by the batch.


1223-1314: LGTM! zset_store_operation correctly implements atomic destination update.

The refactored code properly:

  • Acquires lock on destination before any modifications
  • Creates a single batch for all destination changes
  • Deletes existing destination data (if any) within the batch
  • Writes new result data within the same batch
  • Commits atomically, ensuring no partial state is visible

This is a significant improvement over the previous implementation as it guarantees atomicity for the entire destination update.


1585-1607: LGTM! zremrangebylex batch changes follow consistent pattern.

The implementation correctly batches member/score deletions with meta updates, maintaining the same pattern as other zrem* operations.


1708-1730: LGTM! zremrangebyrank batch changes are consistent.


1811-1833: LGTM! zremrangebyscore batch changes follow the established pattern.

src/storage/src/redis_strings.rs (10)

318-324: LGTM! setrange correctly uses batch for single-key write.

Simple and correct migration to batch abstraction.


389-395: LGTM! append batch changes follow consistent pattern.


581-587: LGTM! set, setex, psetex batch changes are consistent.

All three functions follow the same pattern for atomic writes with TTL handling.


759-765: LGTM! setnx and getset batch changes are consistent.


1063-1078: LGTM! mset correctly batches multiple key-value writes atomically.

The batch ensures all keys are written atomically as required by Redis MSET semantics.


1212-1218: LGTM! incr_decr and incr_decr_float batch changes are consistent.


1429-1435: LGTM! setbit batch changes follow consistent pattern.


1836-1843: LGTM! bitop correctly uses batch for each execution path.

Multiple batch creation points are appropriate here since each code path has a single write operation followed by early return.


2083-2162: LGTM! del_key refactored for atomic batch deletion with proper prefix scanning.

The implementation correctly:

  1. Builds the data key prefix using encode_user_key to match the data key format
  2. Collects all keys to delete first (avoiding borrow conflicts)
  3. Iterates over all relevant column families (Hashes, Sets, Lists, Zsets data and score)
  4. Batches all deletions atomically

The comment on lines 2113-2117 explains the key format well, which is helpful for maintainability.


2233-2280: LGTM! flush_db uses chunked deletion to handle large databases.

The chunked approach with CHUNK_SIZE = 1000 is appropriate for flush operations:

  • Prevents memory exhaustion on large databases
  • Makes incremental progress (earlier chunks persist even if later ones fail)
  • Commits each chunk separately, which is acceptable for a destructive operation like flush

Note: This is intentionally not fully atomic - if a failure occurs mid-flush, some data will already be deleted. This is acceptable behavior for FLUSHDB.

src/storage/src/redis_lists.rs (9)

105-107: LGTM! push_core correctly collects operations before batching.

The two-phase approach (collect in puts/deletes vectors, then batch and commit) avoids borrow conflicts and ensures all operations are committed atomically.


236-245: LGTM! Batch commit order is correct.

The ordering (deletes → puts → meta update → commit) ensures:

  1. Old data is removed before new data is written
  2. Metadata is updated after data operations
  3. All changes are committed atomically

341-353: LGTM! lpop correctly handles empty list case.

The batch properly:

  • Deletes popped element keys
  • Deletes meta key if list becomes empty (consistent with Redis behavior)
  • Updates meta key otherwise
  • Commits atomically

417-429: LGTM! rpop_internal follows consistent pattern.


493-505: LGTM! rpop follows consistent pattern.


719-726: LGTM! lset correctly batches single element update.


813-824: LGTM! ltrim correctly handles delete-all vs trim-range cases.

The use of delete_meta and update_meta flags cleanly separates the two cases:

  • Delete all elements → delete meta key
  • Trim to range → update meta key with new indices

942-955: LGTM! lrem correctly batches element removal and rewrite.

The implementation properly:

  • Deletes all existing elements
  • Writes back remaining elements
  • Handles empty list case by deleting meta

1082-1091: LGTM! linsert correctly batches element rewrite after insertion.

@AlexStocks AlexStocks merged commit d771a7f into arana-db:main Jan 17, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✏️ Feature new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] support batch

2 participants