Skip to content

Release v0.2.0 - #206

Merged
JoshuaChi merged 65 commits into
mainfrom
develop
Dec 31, 2025
Merged

JoshuaChi merged 65 commits into
mainfrom
develop

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented Dec 13, 2025 •

Copy link
Copy Markdown
Contributor

Type

  • New feature
  • Bug Fix

Description

Release v0.2.0

Related Issues

This release includes work from the following issues:

Checklist

  • The code has been tested locally (unit test or integration test)
  • Squash down commits to one or two logical commits which clearly describe the work you've done.

Summary by CodeRabbit

  • New Features

    • Watch API for real-time key-change streams; TTL/lease-based automatic key expiry; embedded mode and local in-process KV client; KV client adds TTL writes and watch support plus leader discovery.
  • Documentation

    • v0.2.0 changelog, comprehensive migration guide, rewritten README with new modes, examples and quickstart.
  • Chores

    • Workspace/workflow reorganization, weekly benchmark workflow and reports, expanded Makefile targets, CI and lint/config updates, new ignore patterns.
  • Tests

    • Large suite of new unit/integration tests and mock utilities covering client, raft, storage and replication.

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

…y log entry. feat #66: Refactor the Errors to distinguish between protocol logic errors and system-level errors.

* feat #43: When a Raft leader is elected, it should first send an empty log entry. 
1. implement verify_leadership_in_new_term 
2. refactor verify_leadership_in_new_term and enforce_quorum_consensus

* feat #66: Refactor the Errors to distinguish between protocol logic errors and system-level errors.
* feat #71: add client usage example

* feat #59: Benchmarking against etcd v3.5, Enhance examples/three-nodes-cluster with Makefile

* fix: #70 Node Logs Exiting Program After Stress Test — Unexpected or Simulated Shutdown
…r-friendly StorageEngine and StateMachine

* feat #59: Refactor state_machine_commit_listener into a separate function to improve visibility across all newly spawned threads.

* feat #79: Add snapshot feature

* fix #90: Refactor: Decouple Client Command Protocol from Raft Internal Log Payload Type

* feat #89: Add auto-discovery support for new learner nodes

* feat #101: Revised learner join process with promotion/fail semantics

* feat #45: Implemented first/last index for term to resolve replication conflicts

* fix #106: Retry leadership noop confirmation until timeout

* feat #102: Automatic node removal 

* feat #109: Enable RPC connection cache

* fix #107: 1. Add back rpc connection params 2. Change parking lot sync lock to Tokio async lock in RaftMembership 3. optimize MembershipGuard for performance. Remove read locker.

* feat #110: 1. add new events SnapshotCreated and LogPurgeCompleted 2. decouple Leader snapshot create to several events 3. refactor raft log index generation from pre_allocate_raft_logs_next_index to pre_allocate_id_range 4. optimize LocalLogBatch structure - based on performance analysis

* feat #119: Make the StorageEngine  trait more developer-friendly

* feat #120: Make the StateMachine trait more developer-friendly

* fix #121: Election: higher term vote request should be updated immediately and reset voted_for

* feat #122: Refactor compress and decompress snapshot logic from StateMachine to StateMachineHandler

* fix #123: Replace global tracing log with trace_test crate

* feat #125: Make rocksdb adaptor as feature. Move sled implementation as example
…tart when path is already locked by another node
* refs #138: optimize to use long lived peer tasks for append entries request only

* refs #138: try to fix race issue - Multiple threads can pass the initial check and all create new appenders

* feat #138: 1. introduce crossbeam 2. optimize buffered_raft_log by creating a flush worker pool with configurable number of workers to handle persistence operations

* feat #138: optimize flush_workers as configure

* feat #138: optimize channel_capacity into configure

* feat #139: work on a reliable monitoring metric for resource utilization

* feat #140: Optimize proto bytes fields to use bytes::Bytes

* feat #140: switch Into<Bytes> to AsRef<[u8]>

* fix #141: Optimize RocksDB write path and Raft log loop for lower latency

* feat 142: Add Read Consistency Policy Support and Lease-Based Read Optimization

* feat #142: update v0.1.4 bench report

* feat #142: add client doc guide

* feat #143: Refactor gRPC compression configuration for Raft transport (performance optimization) - step1: disable server side client rpc compress response

* feat #143: Refactor gRPC compression configuration for Raft transport (performance optimization)

* feat #143: add performance diagram

* feat #138: ready to publish v0.1.4

* feat #138: add ut

* fix #146: buffered_raft_log.rs Code Reviews

* fix #146: update bench report

* fix #145: Bug: Undefined Behavior from Incorrect Lifetime Conversion in Mmap Zero-Copy Path

* fix #148: fix rocksdb storage engine flush bug

* fix #147: fix file state machine bug
Changes:
- Remove commented legacy package config from workspace Cargo.toml
- Update README benchmark reference to v0.2.0 (dengine_comparison_v0.2.0.png)
- Upgrade rustfmt edition from 2021 to 2024
- Clean up outdated v0.2.0 benchmark reports (report_20251205/09/10.md)
- Add final v0.2.0 benchmark report and comparison chart
- Sync version references across all example Cargo.toml files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Makefile (1)

154-155: Fix duplicate comment for clippy target.

Lines 154-155 contain duplicate comment lines describing the clippy target, which may confuse the help parser. Keep a single, descriptive comment.

Apply this diff:

-## clippy               Run Clippy linter (fail on warnings with -D flag)
-## clippy                Run Clippy linter on all crates
+## clippy               Run Clippy linter on all crates (fail on warnings)
🧹 Nitpick comments (3)
benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md (1)

174-174: Minor grammar refinement: "a small number of keys" → "10 keys".

At line 174, the phrase "a small number of keys (10)" can be tightened to simply "10 keys" or "few keys" for clearer, more direct prose.

README.md (1)

80-80: Clarify benchmark methodology and source attribution.

The footnote "*Benchmarks show 51% higher write throughput vs etcd..." references specific numbers but doesn't clearly link to the detailed benchmark report. Consider explicitly directing readers to benches/d-engine-bench/reports/v0.2.0/ for the comprehensive methodology and environment details.

Consider updating to: "Benchmarks show 51% higher write throughput vs etcd in test environments (see benches/d-engine-bench/reports/v0.2.0/ for methodology and environment details). Production results vary by hardware."

Makefile (1)

1-576: Approve comprehensive Makefile structure.

The Makefile provides excellent developer experience with:

  • Clear organization across logical sections (quality, build, test, docs, security, cleanup, troubleshooting)
  • Detailed help output with color-coded messages and workflow examples
  • Environment validation and workspace structure checks
  • Comprehensive test targets (unit, integration, doc, examples, detailed output, verbose, all)
  • Documentation generation with browser automation
  • Security audit targets
  • Composite workflows for common tasks (pre-release, check-all-projects)
  • Color-coded output for improved readability

The overall structure and functionality are well-designed and should significantly improve the development experience.

Consider addressing the static analysis warnings for consistency: while body length warnings are acceptable and don't affect functionality, ensuring all targets are declared .PHONY is a Makefile best practice that improves portability and clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9123484 and f7b0418.

⛔ Files ignored due to path filters (2)
  • benches/d-engine-bench/Cargo.lock is excluded by !**/*.lock
  • benches/d-engine-bench/reports/v0.2.0/dengine_comparison_v0.2.0.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • Cargo.toml (2 hunks)
  • Makefile (1 hunks)
  • README.md (4 hunks)
  • benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md (1 hunks)
  • benches/d-engine-bench/rustfmt.toml (1 hunks)
🧰 Additional context used
🪛 checkmake (0.2.2)
Makefile

[warning] 54-54: Target body for "help" exceeds allowed length of 5 (35).

(maxbodylength)


[warning] 407-407: Target body for "audit" exceeds allowed length of 5 (6).

(maxbodylength)


[warning] 416-416: Target body for "deny" exceeds allowed length of 5 (6).

(maxbodylength)


[warning] 448-448: Target body for "troubleshoot" exceeds allowed length of 5 (34).

(maxbodylength)


[warning] 18-18: Missing required phony target "clean"

(minphony)


[warning] 115-115: Target "install" should be declared PHONY.

(phonydeclared)


[warning] 397-397: Target "doc" should be declared PHONY.

(phonydeclared)

🪛 LanguageTool
benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md

[style] ~174-~174: Specify a number, remove phrase, use “a few”, or use “some”
Context: ...st simulates contention scenarios where a small number of keys (10) receive high request volume. ...

(SMALL_NUMBER_OF)

🪛 markdownlint-cli2 (0.18.1)
benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md

178-178: Multiple headings with the same content

(MD024, no-duplicate-heading)

⏰ 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: audit
  • GitHub Check: Analyze (rust)
  • GitHub Check: audit
🔇 Additional comments (4)
Cargo.toml (2)

50-76: Approve profile configuration tuning.

The profile configurations are well-tuned for different build scenarios:

  • dev: Low optimization (opt-level=1) for fast iteration, incremental compilation enabled
  • release: Aggressive optimization (opt-level='z' for size, LTO, single codegen unit) for production
  • test: Balanced approach with incremental compilation and multi-core codegen

This is a good setup for a Rust workspace.


2-10: All workspace members are correctly declared and paths are valid.

Verification confirms all 6 members exist with proper Cargo.toml files: d-engine, d-engine-proto, d-engine-core, d-engine-client, d-engine-docs, and d-engine-server. Each crate name matches its path reference.

README.md (1)

10-11: Verify new public APIs and features are properly exported.

The README introduces several new public APIs and features that must exist and be properly exported:

  1. EmbeddedEngine (lines 22, 107, 116): Public struct in d-engine-server at src/embedded/mod.rs, re-exported via d-engine::prelude
  2. LocalKvClient (line 23): Public struct in d-engine-server, re-exported via d-engine::prelude
  3. prelude module (line 101): Public module at d-engine/src/lib.rs:203 that exports EmbeddedEngine, LocalKvClient, Node, NodeBuilder, and related types (conditional on features)
  4. Feature flags (lines 48, 51): client and server features properly defined in d-engine/Cargo.toml as optional dependencies
  5. TTL/Lease (lines 20-21): Lease trait re-exported from d-engine-core, with DefaultLease implementation in d-engine-server/src/storage/lease.rs
  6. Watch API (lines 20-21): Implemented via watch() method in EmbeddedEngine at src/embedded/mod.rs:322 and leader_notifier() for leader change notifications

All mentioned example directories exist: examples/quick-start/, examples/single-node-expansion/, examples/sled-cluster/, examples/three-nodes-cluster/.

Makefile (1)

18-22: Add missing .PHONY declarations.

Several targets are missing from the .PHONY declaration at lines 18-22, including install (line 115) and doc/doc-check (lines 397-400). Update the .PHONY list to include all non-file targets.

Apply this diff to add missing phony targets:

 .PHONY: help all check fmt fmt-check clippy clippy-fix test test-unit \
         test-integration test-doc bench clean doc build build-release \
         pre-release install check-env audit deny troubleshoot \
         test-crate test-examples test-detailed \
-        docs docs-all docs-check docs-check-all
+        docs docs-all docs-check docs-check-all \
+        install-tools doc doc-check check-workspace check-examples \
+        check-benches check-all-projects build-examples fmt-fix \
+        clippy-excluded test-verbose fix clean-deps

Likely an incorrect or invalid review comment.

Comment thread benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md Outdated
- Fix typo: rename backgroup → background in snapshot transfer
- Remove obsolete storage_test.rs
- Update client error handling and KV operations
- Fix network module exports
- Update candidate state and state machine handler
- Fix single-node cluster examples documentation
- Update benchmark workflow configuration
Cache cluster metadata in LeaderState to eliminate 3 async calls per write.

Performance: +2-3% throughput, -13-21% p99 latency vs v0.2.0

Changes:
- Add ClusterMetadata { is_single_node, total_voters } cache
- Initialize on leader election, update on membership changes
- Pass to replication handler to avoid repeated queries
- Fix 6 test failures due to missing metadata initialization

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (7)
d-engine-client/src/grpc_kv_client.rs (1)

376-395: Potential panic if channel pool is empty.

rng.gen_range(0..channels.len()) will panic with an empty range if get_all_channels() returns an empty vector (e.g., during initialization or when all nodes are disconnected).

Add a guard for empty channels:

     pub(super) async fn make_client(
         &self
     ) -> std::result::Result<RaftClientServiceClient<Channel>, ClientApiError> {
         let client_inner = self.client_inner.load();
 
         // Balance from read clients
         let mut rng = StdRng::from_entropy();
         let channels = client_inner.pool.get_all_channels();
+        if channels.is_empty() {
+            return Err(ClientApiError::Network("No available channels".into()));
+        }
         let i = rng.gen_range(0..channels.len());
 
         let mut client = RaftClientServiceClient::new(channels[i].clone());
d-engine-core/src/replication/mod.rs (1)

189-220: Docs reference AppendEntriesResponse::{success,higher_term,conflict}—likely stale after helper removal.

check_append_entries_request_is_legal’s docs still point at constructor-style APIs that (per this PR) were removed from AppendEntriesResponse. Update doc return bullets to match the new construction path (e.g., “returns an AppendEntriesResponse with success_result/conflict_result/term semantics…”), so downstream implementors don’t chase non-existent helpers.

d-engine-core/src/state_machine_handler/default_state_machine_handler.rs (1)

1199-1224: Critical: Hardcoded offset breaks configurable prefix.

Line 1211 uses a hardcoded offset of 9 bytes, assuming the prefix is always "snapshot-". However, the function now accepts a configurable snapshot_dir_prefix parameter. If a different prefix is used (e.g., "snap-", "backup-", or any non-9-character prefix), the parsing will:

  • Slice at the wrong position, producing incorrect index/term values
  • Potentially panic if the string is shorter than 9 characters
  • Cause snapshot cleanup and management to fail

Apply this diff to use the dynamic prefix length:

-    // Remove fixed parts
-    let core = &name[9..name.len()]; // "snapshot-".len() = 9,
+    // Remove prefix (now configurable)
+    let core = &name[snapshot_dir_prefix.len()..];
d-engine-client/src/error.rs (1)

150-205: Preserve server message in fallback case for better diagnostics.

The fallback handler (_ => Self::Business) drops status.message() when mapping unhandled status codes, replacing it only with a code debug representation. This makes diagnosing server-side failures harder. Include the original message:

            _ => Self::Business {
                code: ErrorCode::Uncategorized,
-                message: format!("Unhandled status code: {code:?}"),
+                message: format!("Unhandled status code: {code:?}: {message}"),
                required_action: None,
            },
d-engine-core/src/raft_role/candidate_state.rs (1)

243-284: Avoid self.voted_for().unwrap() in vote-request handling (panic risk).
If voted_for() can ever error (storage, state init), Candidate will crash instead of rejecting / stepping down cleanly.

-                if ctx.election_handler().check_vote_request_is_legal(
+                if ctx.election_handler().check_vote_request_is_legal(
                     &vote_request,
                     my_term,
                     last_log_index,
                     last_log_term,
-                    self.voted_for().unwrap(),
+                    self.voted_for()?,
                 ) {
d-engine-core/src/replication/replication_handler.rs (2)

564-614: New public API: avoid assert! panics in generate_new_entries.
assert!(!id_range.is_empty()) (Line 579) will abort the process if allocation ever fails. For a pub async fn returning Result, prefer non-panicking handling (at minimum debug_assert! + a structured error path).


686-706: New public helper must not panic: remove .unwrap() on protobuf encoding.
cmd.encode(&mut buf).unwrap() (Line 699) can panic; for a public API, use the infallible encode_to_vec() method instead.

 pub fn client_command_to_entry_payloads(commands: Vec<WriteCommand>) -> Vec<EntryPayload> {
     commands
         .into_iter()
         .map(|cmd| {
-            let mut buf = BytesMut::with_capacity(cmd.encoded_len());
-            cmd.encode(&mut buf).unwrap();
-
             EntryPayload {
-                payload: Some(Payload::Command(buf.freeze())),
+                payload: Some(Payload::Command(cmd.encode_to_vec().into())),
             }
         })
         .collect()
 }
♻️ Duplicate comments (2)
d-engine-core/src/network/mod.rs (1)

9-13: Typo fix from past review has been addressed.

The module has been correctly renamed from backgroup_snapshot_transfer to background_snapshot_transfer.

d-engine-client/src/error.rs (1)

104-147: Default transport failure: make invalid-URI detection robust + align retry hint.

  • err.to_string().contains("invalid uri") is case-sensitive; tonic errors often say “invalid URI”, so this can silently misclassify.
  • The fallback now correctly uses Uncategorized (good), but retry_after_ms: Some(5000) implies retryability despite being “unclassified”; past guidance suggested None.
@@
-        // Check for invalid address errors
-        if err.to_string().contains("invalid uri") {
+        // Check for invalid address errors (be case-insensitive)
+        let err_s = err.to_string();
+        if err_s.to_ascii_lowercase().contains("invalid uri") {
             return Self::Network {
                 code: ErrorCode::InvalidAddress,
-                message: format!("Invalid address: {err}"),
+                message: format!("Invalid address: {err}"),
                 retry_after_ms: None, // Not retryable - needs address correction
                 leader_hint: None,
             };
         }
@@
         Self::Network {
             code: ErrorCode::Uncategorized,
             message: format!("Transport error: {err}"),
-            retry_after_ms: Some(5000),
+            retry_after_ms: None,
             leader_hint: None,
         }
🧹 Nitpick comments (14)
d-engine-core/src/storage/lease.rs (3)

114-141: Clarify caller responsibilities for expired keys.

Both get_expired_keys and on_apply return Vec<Bytes> of expired keys, but the documentation doesn't explicitly state what the caller should do with them. While it's implied that these keys should be deleted from the main storage, making this contract explicit would improve API clarity.

Consider adding to the documentation:

 /// # Returns
-/// List of expired keys (keys are removed from lease tracking)
+/// List of expired keys (keys are removed from lease tracking).
+/// The caller is responsible for deleting these keys from the main storage.

Similar clarification for on_apply would be helpful.


182-188: Consider returning Result from to_snapshot for error transparency.

The method returns Vec<u8> rather than Result<Vec<u8>>, preventing serialization errors from being propagated. The implementation in d-engine-server uses .unwrap_or_default(), which silently returns an empty snapshot on failure. This could mask snapshot creation issues.

While reload properly returns Result<()> for deserialization errors, the asymmetry means snapshot creation failures go unnoticed until restoration fails.

Consider changing the signature to:

-fn to_snapshot(&self) -> Vec<u8>;
+fn to_snapshot(&self) -> Result<Vec<u8>>;

This allows callers to handle serialization failures explicitly.


156-169: Documentation is slightly imprecise about performance.

The documentation states "Should be O(1) or O(log N) at most" (line 165), but the implementation in d-engine-server samples the first 10 entries, which is O(1). The "or O(log N)" part might be unnecessary given the heuristic nature of this check.

Consider tightening the performance documentation:

 /// # Performance
-/// Should be O(1) or O(log N) at most.
+/// Should be O(1) - typically a fast heuristic check (e.g., sampling a few entries).
d-engine-client/src/grpc_kv_client.rs (1)

99-136: Consider extracting shared write logic to reduce duplication.

The put_with_ttl implementation mirrors put except for the WriteCommand construction. The request building, leader client acquisition, and response handling are duplicated.

Consider extracting a helper method:

+    async fn execute_write(&self, commands: Vec<WriteCommand>, op_name: &str) -> std::result::Result<(), ClientApiError> {
+        let client_inner = self.client_inner.load();
+        let request = ClientWriteRequest {
+            client_id: client_inner.client_id,
+            commands,
+        };
+        let mut client = self.make_leader_client().await?;
+        match client.handle_client_write(request).await {
+            Ok(response) => {
+                debug!("[:KvClient:{}] response: {:?}", op_name, response);
+                response.get_ref().validate_error()
+            }
+            Err(status) => {
+                error!("[:KvClient:{}] status: {:?}", op_name, status);
+                Err(status.into())
+            }
+        }
+    }

This would simplify put, put_with_ttl, and delete to single command construction + helper call.

d-engine-client/src/error.rs (3)

55-98: Delete commented-out public enums instead of keeping them in-tree.
Large commented blocks (NetworkErrorType/ProtocolErrorType/etc.) add noise and tend to rot; rely on git history or reintroduce with a proper deprecation cycle if needed.


239-275: Avoid manual “JSON-like” parsing for x-raft-leader metadata.
The current split-by-, / split-by-: parser will break if values contain commas/colons, or if encoding changes slightly. If the header is JSON, parse it as JSON; if it’s not JSON, standardize it (e.g., base64 JSON) or use a structured delimiter format.

@@
 fn parse_leader_from_metadata(status: &Status) -> Option<LeaderInfo> {
     status
         .metadata()
         .get("x-raft-leader")
         .and_then(|v| v.to_str().ok())
-        .and_then(|s| {
-            // Manually parse JSON-like string
-            let mut id = None;
-            let mut address = None;
-            let mut last_contact = None;
-
-            // Remove whitespace and outer braces
-            let s = s.trim().trim_start_matches('{').trim_end_matches('}');
-
-            // Split into key-value pairs
-            for pair in s.split(',') {
-                let pair = pair.trim();
-                if let Some((key, value)) = pair.split_once(':') {
-                    let key = key.trim().trim_matches('"');
-                    let value = value.trim().trim_matches('"');
-
-                    match key {
-                        "id" => id = Some(value.to_string()),
-                        "address" => address = Some(value.to_string()),
-                        "last_contact" => last_contact = value.parse().ok(),
-                        _ => continue,
-                    }
-                }
-            }
-
-            Some(LeaderInfo {
-                id: id?,
-                address: address?,
-                last_contact: last_contact?,
-            })
-        })
+        .and_then(|s| serde_json::from_str::<LeaderInfo>(s).ok())
 }

277-397: Consider mapping ErrorCode::Uncategorized to General (not Business).
Right now ErrorCode::Uncategorized becomes ClientApiError::Business, which can drive callers toward “client must change behavior” semantics for unknown failures. General seems more appropriate unless you have a specific reason.

d-engine-client/src/pool.rs (1)

153-187: Improve final failure signal from load_cluster_metadata by including last error.
Currently you log each failure but return a generic ClusterUnavailable. Consider capturing the last error string and embedding it in ClientApiError (or attach as context) to help operators.

d-engine-core/src/raft_role/role_state.rs (1)

322-353: create_not_leader_response() likely does an avoidable O(n) scan; prefer a direct leader lookup if Membership supports it.
Right now it awaits members() then iter().find(...). If there’s a retrieve_node_meta(id)/map-like accessor, it’ll be cheaper and clearer.

d-engine-core/src/raft_role/candidate_state.rs (1)

571-575: StepDownSelfRemoved as unreachable!() is OK, but consider a defensive fallback in release builds.
If routing ever regresses and this becomes reachable, this will crash the node. A warn! + BecomeFollower(None) fallback can make the system more resilient.

d-engine-core/src/raft_role/leader_state.rs (2)

1235-1253: Step-down on self-removal is good; consider also clearing current_leader_id to avoid stale redirection.
After stepping down, clients hitting this node could still be redirected to the old leader id until the next heartbeat updates state elsewhere.

             RaftEvent::StepDownSelfRemoved => {
+                self.shared_state().clear_current_leader();
                 warn!(
                     "[Leader-{}] Removed from cluster membership, stepping down to Follower",
                     self.node_id()
                 );

1008-1032: snapshot_in_progress uses mixed orderings (Acquire/Release then SeqCst); consider standardizing.
Not wrong, but it’s harder to reason about than sticking to Acquire loads + Release stores (or SeqCst consistently).

d-engine-core/src/replication/replication_handler.rs (2)

543-549: New public struct: add #[non_exhaustive] (or keep fields private) to reduce semver breakage.
Making ReplicationData and all fields pub locks you into this shape; #[non_exhaustive] is a low-cost guardrail.

 #[derive(Debug)]
+#[non_exhaustive]
 pub struct ReplicationData {
     pub leader_last_index_before: u64,
     pub current_term: u64,
     pub commit_index: u64,
     pub peer_next_indices: HashMap<u32, u64>,
 }

617-631: Public prepare_peer_entries returns DashMap + clones input—document ownership/perf expectations.
Now that this is pub, callers may assume it’s cheap; but it clones new_entries and later clones per peer. A short doc note (or a future API taking shared buffers) will prevent misuse.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7b0418 and 1b55a6f.

📒 Files selected for processing (18)
  • .github/workflows/benchmark.yml (1 hunks)
  • d-engine-client/src/error.rs (8 hunks)
  • d-engine-client/src/grpc_kv_client.rs (6 hunks)
  • d-engine-client/src/kv_client.rs (1 hunks)
  • d-engine-client/src/pool.rs (10 hunks)
  • d-engine-client/src/pool_test.rs (1 hunks)
  • d-engine-core/src/network/background_snapshot_transfer.rs (1 hunks)
  • d-engine-core/src/network/background_snapshot_transfer_test.rs (2 hunks)
  • d-engine-core/src/network/mod.rs (7 hunks)
  • d-engine-core/src/raft.rs (10 hunks)
  • d-engine-core/src/raft_role/candidate_state.rs (16 hunks)
  • d-engine-core/src/raft_role/leader_state.rs (40 hunks)
  • d-engine-core/src/raft_role/mod.rs (9 hunks)
  • d-engine-core/src/raft_role/role_state.rs (7 hunks)
  • d-engine-core/src/replication/mod.rs (3 hunks)
  • d-engine-core/src/replication/replication_handler.rs (10 hunks)
  • d-engine-core/src/state_machine_handler/default_state_machine_handler.rs (12 hunks)
  • d-engine-core/src/storage/lease.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • d-engine-client/src/pool_test.rs
  • d-engine-core/src/raft.rs
🧰 Additional context used
🧬 Code graph analysis (4)
d-engine-core/src/network/mod.rs (1)
d-engine-core/src/network/background_snapshot_transfer.rs (1)
  • mpsc (83-83)
d-engine-core/src/replication/replication_handler.rs (1)
d-engine-core/src/utils/cluster.rs (1)
  • is_majority (8-13)
d-engine-core/src/storage/lease.rs (2)
d-engine-server/src/storage/lease.rs (10)
  • register (200-214)
  • unregister (221-227)
  • is_expired (234-243)
  • get_expired_keys (252-269)
  • on_apply (280-293)
  • has_lease_keys (300-302)
  • may_have_expired_keys (312-329)
  • len (336-338)
  • to_snapshot (345-354)
  • reload (363-395)
d-engine-core/src/storage/state_machine.rs (2)
  • len (62-62)
  • is_empty (66-68)
d-engine-core/src/raft_role/mod.rs (2)
d-engine-core/src/raft_role/leader_state.rs (6)
  • new (105-113)
  • new (2008-2049)
  • update_voted_for (290-295)
  • voted_for (284-286)
  • match_index (327-332)
  • next_index (297-306)
d-engine-core/src/raft_role/role_state.rs (7)
  • update_voted_for (295-300)
  • current_term (230-232)
  • voted_for (289-291)
  • commit_index (242-244)
  • match_index (65-71)
  • node_id (42-44)
  • next_index (49-55)
⏰ 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: audit
  • GitHub Check: Lint and Format Check
  • GitHub Check: Analyze (rust)
  • GitHub Check: Lint and Format Check
  • GitHub Check: audit
🔇 Additional comments (36)
.github/workflows/benchmark.yml (2)

3-4: Permissions block properly implemented, addressing previous CodeQL feedback.

The explicit permissions: contents: read block correctly restricts the GITHUB_TOKEN to read-only access, which aligns with the principle of least privilege. This addresses the earlier security concern flagged by CodeQL.


1-83: Well-structured benchmark workflow with sound caching and artifact strategies.

The workflow is well-designed for performance testing:

  • Scheduling & triggers (lines 7–13): Weekly runs with manual override provide flexibility for on-demand benchmarking.
  • Rust setup (lines 30–36): Stable toolchain + protobuf-compiler installation is correct for the workspace.
  • Caching (lines 38–42): Custom cache key benchmark-* appropriately isolates benchmark cache from test runs.
  • Full history checkout (line 28): fetch-depth: 0 enables meaningful performance comparisons across commits.
  • Output & artifacts (lines 62–83): Criterion results + log tee + 30-day retention and clear summary messaging support post-hoc analysis and iteration.
d-engine-core/src/network/background_snapshot_transfer.rs (1)

30-33: LGTM! Import migration to external proto crate is consistent.

The migration from crate::proto::server::storage::* to d_engine_proto::server::storage::* aligns with the PR's architectural goal of using the external proto crate.

d-engine-core/src/network/background_snapshot_transfer_test.rs (2)

24-46: LGTM! Well-structured test helper for creating snapshot streams.

The helper function properly:

  1. Creates test chunks with metadata on seq 0
  2. Maps tonic Status errors to NetworkError appropriately
  3. Returns a properly boxed stream

19-22: Import migration to d_engine_proto is consistent with main module.

The test file properly imports SnapshotChunk and SnapshotResponse from d_engine_proto::server::storage, matching the production code's import paths.

d-engine-core/src/network/mod.rs (4)

36-44: Verify public API exposure is intentional for AppendResults.

The visibility has been widened from pub(crate) to pub. Ensure this struct is properly documented and re-exported from the crate's public API surface if intended for external use.


93-98: LGTM! Extended automock availability for integration testing.

The #[cfg_attr(any(test, feature = "test-utils"), automock)] pattern correctly enables MockTransport both for internal tests and for downstream crates using the test-utils feature.


24-32: Import migration to d_engine_proto is consistent and complete.

All protobuf types used in the Transport trait have been properly migrated from internal paths to d_engine_proto::server::*.


305-309: Confirm grpc_task_with_timeout_and_exponential_backoff should be public API.

This utility function is now pub instead of pub(crate). While useful for consumers implementing custom gRPC operations, verify this is intentionally part of the v0.2.0 public API and document its usage accordingly.

d-engine-core/src/storage/lease.rs (2)

1-24: LGTM! Excellent module documentation.

The module documentation provides clear design philosophy, architecture overview, and concurrency guidelines. This sets appropriate expectations for trait implementers.


73-87: Parameter type choice is appropriate.

The register method takes ownership of key: Bytes while other methods (like unregister, is_expired) use &[u8]. This asymmetry is intentional and efficient: register needs to store the key, while lookups only need a reference.

d-engine-client/src/kv_client.rs (2)

1-51: Well-documented trait interface with clear consistency guarantees.

The module documentation clearly explains the design principles, implementations, and performance characteristics. The trait's thread safety requirements (Send + Sync) and consistency guarantees are well-specified.


74-197: LGTM!

Method signatures are consistent with idiomatic Rust async trait patterns. Using impl AsRef<[u8]> + Send provides flexibility for callers while maintaining type safety. The idempotent delete semantics are correctly documented.

d-engine-client/src/grpc_kv_client.rs (3)

14-36: LGTM!

Imports are well-organized with clear aliasing (KvClient as CoreKvClient). The struct definition with Arc<ArcSwap<ClientInner>> enables efficient concurrent access with runtime-updatable configuration.


310-358: LGTM!

The watch implementation correctly uses load-balanced client selection since followers can serve watch streams. Documentation clearly notes the v1 limitation (no prefix/range watch support).


398-447: LGTM!

The CoreKvClient trait implementation correctly delegates to the inherent methods and properly maps ClientResult to Bytes (extracting the .value field). Error propagation via Into::into is clean and type-safe.

d-engine-core/src/replication/mod.rs (2)

105-113: No action required; parameter change is complete and properly integrated.

All implementors and call sites have been updated correctly. The single ReplicationCore implementation in replication_handler.rs includes the new cluster_metadata parameter (line 65) and actively uses it on line 76 (let is_single_node = cluster_metadata.is_single_node;). The production call site in leader_state.rs (lines 1383-1389) properly passes &self.cluster_metadata, and all 30+ test cases pass the parameter correctly.


21-26: No issues found. The test-utils feature properly gates mockall as an optional dependency in Cargo.toml, and all #[cfg(any(test, feature = "test-utils"))] guards correctly restrict mockall imports to test/test-utils builds only. The async-trait crate is a non-optional dependency (needed for all async trait definitions), so tonic::async_trait is unconditionally available—this is correct. Tonic v0.12.3 is declared with proper features. Test-only dependencies do not leak into production.

d-engine-core/src/state_machine_handler/default_state_machine_handler.rs (5)

111-113: Good design for optional feature integration.

The use of Option<Arc<WatchManager>> provides zero-overhead when watch functionality is disabled, with clear documentation explaining the behavior.


186-198: LGTM: Watcher notification correctly guarded by success check.

The previous concern about notifying watchers even on error has been properly addressed. The notification now only occurs when apply_result.is_ok() is true, ensuring watchers are not notified about failed operations.


709-739: LGTM: Clean watcher notification implementation.

The method correctly:

  • Gracefully handles decode failures with if let Ok
  • Uses pattern matching for operation variants
  • Maintains performance with inline attribute and early returns

The silent skipping of decode errors is appropriate here, as not all entries may contain decodable WriteCommands.


746-796: LGTM: Constructor properly updated with test helper.

The changes correctly:

  • Add the watch_manager parameter to the main constructor
  • Initialize all fields including the new watch_manager
  • Provide a test-only new_without_watch helper for backward compatibility in tests

The #[cfg(test)] attribute appropriately limits the helper to test builds.


400-476: LGTM: Snapshot cleanup properly parameterized.

The cleanup method now correctly accepts a configurable snapshot_dir_prefix parameter and threads it through to parse_snapshot_dirname calls, both for direct filenames and .tar.gz suffixed variants.

d-engine-client/src/pool.rs (2)

189-214: Good fix: “no leader” now maps to retryable ClusterUnavailable.
This matches typical “election in progress / temporarily unavailable” semantics and should improve client retry behavior.


39-54: Returning/storing current_leader_id looks correct; ensure callers treat it as advisory.

current_leader_id is snapshotted at pool build/refresh time and used only during initialization to map node ID to leader address. It's exposed via get_leader_id() for metadata queries, not for per-request routing. Staleness is handled naturally by subsequent RPC redirects when the leader changes.

d-engine-core/src/raft_role/role_state.rs (3)

394-424: Leader discovery notification gating looks good; double-check correctness of “leader committed” semantics.
You now (1) atomically store leader_id, (2) persist/record VotedFor{committed:true}, and (3) emit LeaderDiscovered only when update_voted_for() reports a new committed transition. That’s a solid de-dupe strategy—just ensure SharedState::update_voted_for() is the single source of truth for “committed leader” and is persisted where required.


35-37: No action required — all callers of update_voted_for already properly handle the Result<bool> return type. The API migration is complete with no compilation breaks or missing updates in implementations, tests, or call sites.


89-96: This concern is already properly addressed in the code.

init_cluster_metadata() is correctly called in the RoleEvent::BecomeLeader handler (raft.rs:343) immediately after the Leader role is created and before any hot paths execute. The call sequence ensures cluster topology is cached before process_batch and other leader operations access cluster_metadata for quorum calculations. Error handling with the ? operator propagates any failures, and the Leader implementation correctly populates is_single_node and total_voters based on the membership configuration.

d-engine-core/src/raft_role/candidate_state.rs (1)

390-398: Returning NOT_LEADER with leader metadata on Candidate proposals is the right UX.
This aligns Candidate behavior with redirection and avoids clients waiting on election completion.

d-engine-core/src/raft_role/mod.rs (3)

79-88: Atomic leader-id ordering upgraded to Acquire/Release: good.
This matches the intended “publish leader info / observe leader info” pattern for cross-thread visibility.

Also applies to: 146-167


190-214: SharedState::update_voted_for() semantics look consistent with event-driven leader discovery.
The “only true on committed transition/leader-term change” contract is clear and matches how roles consume it.


48-61: HardState: Copy is valid; VotedFor implements Copy and serde traits.
The proto-generated VotedFor struct explicitly derives Clone, Copy, PartialEq, ::prost::Message, serde::Serialize, serde::Deserialize. All its fields (u32, u64, bool) are primitives, making Copy safe. Option<VotedFor> is therefore also Copy. The current derives on HardState are correct and the code compiles without issues.

d-engine-core/src/raft_role/leader_state.rs (1)

500-520: Verify comment: ensure voters() excludes self and remove the off-by-one concern.

The voters.len() + 1 calculation is correct—membership.voters() explicitly excludes the leader node (filtered by node.id != self.node_id), so adding 1 accounts for self. This is not an off-by-one error.

However, a maintainability concern remains: update_cluster_metadata() only updates total_voters and does not recompute is_single_node, while init_cluster_metadata() computes both. Since is_single_node_cluster() derives from the immutable initial_cluster_size, desynchronization is impossible at runtime. Consider adding a comment in update_cluster_metadata() explaining that is_single_node is immutable configuration and does not need recomputation, to prevent future confusion or refactoring mistakes.

d-engine-core/src/replication/replication_handler.rs (3)

634-671: build_append_request logic looks consistent with follower “virtual log” handling.
Defaulting to (prev_log_index, prev_log_term) = (0, 0) aligns with check_append_entries_request_is_legal’s special-case accept path.


60-67: Verify is_single_node consistency with actual cluster membership (lines 74-96).
cluster_metadata.is_single_node is used to determine quorum behavior; if this field diverges from the authoritative membership state, replication can be incorrectly skipped. Ensure either: (1) is_single_node is derived from Membership at call time, or (2) there is an assertion verifying consistency with Membership when cluster_metadata is instantiated.


149-164: No action needed. Single-node commit index advancement is correctly implemented with a dedicated code path that does not depend on peer match indices.

Comment thread d-engine-core/src/replication/mod.rs Outdated
Comment on lines +59 to 60
#[cfg_attr(any(test, feature = "test-utils"), automock)]
#[async_trait]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

test-utils automock is enabled, but tests modules are still #[cfg(test)] only.

If the goal is to compile/use mocks under --features test-utils outside cfg(test) (e.g., integration tests in other crates), consider also widening local test module gating (e.g., batch_buffer_test) or documenting that test-utils only affects mock generation, not unit tests in this crate.

Also applies to: 13-15

🤖 Prompt for AI Agents
In d-engine-core/src/replication/mod.rs around lines 59-60 (and also apply same
change for lines 13-15), the automock attribute is enabled for the "test-utils"
feature but the test modules themselves remain gated with #[cfg(test)],
preventing mocks from being available when building with --features test-utils
outside of unit-test compilation; either change the test module gating to
#[cfg(any(test, feature = "test-utils"))] (or equivalent per-module cfg) so
mocks are compiled when the feature is enabled, or update crate documentation to
state that "test-utils" only controls mock generation and does not enable the
internal test modules — implement the cfg change for relevant test modules if
you want mocks usable by integration tests in other crates.

Comment thread d-engine-core/src/replication/replication_handler.rs
…ale cache

Bug: update_cluster_metadata() only updated total_voters, missing is_single_node.
Caused test_embedded_node_rejoin to hang intermittently.

Fix:
- Update BOTH is_single_node and total_voters in update_cluster_metadata()
- Call update_cluster_metadata() after learner promotion
- Add unit test to catch this bug
Race condition: gRPC servers start async, election RPCs timeout before ready.

Solution: Increase test election timeout 100ms→2000ms, retries 3→5.

Only affects tests/common/mod.rs test configuration.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
d-engine-core/src/raft_role/leader_state.rs (1)

1599-1608: Consider replacing println! with structured logging.

While the "Plan B" print statements serve immediate visibility needs, consider using the existing tracing infrastructure (info!/warn!) for consistency and production deployability.

Also applies to: 1985-1992

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b55a6f and bcf2239.

📒 Files selected for processing (3)
  • d-engine-core/src/raft_role/leader_state.rs (40 hunks)
  • d-engine-core/src/raft_role/leader_state_test.rs (1 hunks)
  • d-engine-core/src/raft_role/mod.rs (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
d-engine-core/src/raft_role/mod.rs (2)
d-engine-core/src/raft_role/leader_state.rs (6)
  • new (105-113)
  • new (2014-2055)
  • update_voted_for (290-295)
  • voted_for (284-286)
  • match_index (327-332)
  • next_index (297-306)
d-engine-core/src/raft_role/role_state.rs (5)
  • update_voted_for (295-300)
  • voted_for (289-291)
  • match_index (65-71)
  • node_id (42-44)
  • next_index (49-55)
🪛 GitHub Actions: CI
d-engine-core/src/raft_role/leader_state_test.rs

[error] 11-11: Rustfmt would modify this file: formatting changes detected (diffs shown at lines 11, 18, 27, 34, 41). Run 'cargo fmt' to apply formatting.

⏰ 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: audit
  • GitHub Check: Analyze (rust)
  • GitHub Check: audit
🔇 Additional comments (9)
d-engine-core/src/raft_role/mod.rs (4)

77-170: LGTM: Hot-path leadership tracking with correct memory ordering.

The atomic current_leader_id field with Acquire/Release semantics properly synchronizes with persistent state updates, addressing the previous review concern about memory ordering.


193-217: LGTM: Leader commitment detection logic is sound.

The boolean return value correctly identifies transitions to a committed leader vote, enabling event-driven leader discovery without hot-path overhead.


310-368: LGTM: Test-only accessor methods properly gated.

The cfg(any(test, feature = "test-utils")) guards correctly restrict these internal accessors to test contexts.


15-15: LGTM: Public API expansion aligns with v0.2.0 objectives.

The newly public exports (ClusterMetadata, as_i32, current_term, QuorumVerificationResult) support the broader API surface documented in the release.

Also applies to: 267-267, 305-305, 421-421

d-engine-core/src/raft_role/leader_state.rs (5)

116-127: LGTM: ClusterMetadata struct design is sound.

The cached topology metadata approach avoids repeated async membership queries in hot paths, with clear documentation of immutability constraints.


1416-1427: LGTM: Single-node commit optimization is correct.

Single-node clusters can commit immediately (quorum of 1), while multi-node clusters use the existing majority-based calculation. The cached metadata avoids async calls in this hot path.


1930-1933: LGTM: Cluster metadata updated after successful join.

Updating the cached metadata after membership changes ensures the hot-path commit logic uses current topology information.


2440-2442: LGTM: Leader ID set immediately in role transition.

Setting current_leader_id atomically during the CandidateState → LeaderState conversion ensures the hot-path current_leader() reads are correct from the moment the new leader is created.


500-520: > Likely an incorrect or invalid review comment.

Comment thread d-engine-core/src/raft_role/leader_state_test.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
cliff.toml (1)

66-67: Consider removing the redundant commit_parser rule.

Line 67 with the docs? regex pattern matches both "doc" and "docs" followed by an issue number, making line 66's ^doc #\\d+: rule redundant. Both rules assign to the same "Documentation" group.

If line 66 is intended only as a comment or historical reference, consider removing it to adhere to the DRY principle and simplify the configuration:

  { message = "^feat #\\d+:", group = "Features" },
  { message = "^fix #\\d+:", group = "Bug Fixes" },
  { message = "^refs #\\d+:", group = "Work in progress" },
- { message = "^doc #\\d+:", group = "Documentation" },
  { message = "^docs? #\\d+:", group = "Documentation" },
  { message = "^perf #\\d+:", group = "Performance" },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcf2239 and f601479.

📒 Files selected for processing (2)
  • .github/workflows/commit-message-check.yml (1 hunks)
  • cliff.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/commit-message-check.yml
⏰ 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: audit
  • GitHub Check: Analyze (rust)
  • GitHub Check: audit

…tations

- Add Project Status section to README (pre-1.0 + compatibility promise)
- Simplify MIGRATION_GUIDE (remove NodeBuilder API, keep WAL format only)
- Streamline CHANGELOG v0.2.0 highlights (code examples + concise features)
- Update benchmark reports with actual test data (Dec 13, 2025)
- Restructure performance report (TL;DR first, detailed results collapsed)

Focus: Lower cognitive load for new users while maintaining transparency

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
README.md (1)

259-259: Fix typo in license section.

Line 259 contains "d-eninge" which should be "d-engine".

Apply this diff:

-d-eninge is licensed under the terms of the [MIT License](https://en.wikipedia.org/wiki/MIT_License#License_terms)
+d-engine is licensed under the terms of the [MIT License](https://en.wikipedia.org/wiki/MIT_License#License_terms)
🧹 Nitpick comments (5)
benches/d-engine-bench/reports/v0.1.4/report_20251205_remeasured.md (1)

3-3: Optional: Apply consistent date formatting per style guide.

LanguageTool suggests that some style guides recommend commas to set off the year in month-day-year date format. If your project follows a formal style guide, consider updating both date mentions.

Apply this diff to add commas after the year:

-**Date:** December 13, 2025  
+**Date:** December 13, 2025,
-**Test Date:** December 13, 2025  
+**Test Date:** December 13, 2025,

Also applies to: 185-185

d-engine-client/src/kv_client.rs (3)

20-25: Example return type inconsistent with actual API.

The example uses Result<()> but trait methods return KvResult<()>. Consider updating for consistency:

-//! async fn store_config<C: KvClient>(client: &C) -> Result<()> {
+//! async fn store_config<C: KvClient>(client: &C) -> KvResult<()> {

80-105: Consider documenting TTL edge case behavior.

The behavior of ttl_secs = 0 is not specified. Does it mean "no expiration" or "immediate expiration"? Clarifying this edge case in the documentation would help implementers maintain consistent semantics.


168-171: API signature inconsistency with other methods.

get_multi requires &[Bytes] while other methods accept impl AsRef<[u8]> + Send. This forces callers to pre-convert keys to Bytes for batch operations, whereas single-key operations accept byte slices directly.

If this is intentional for performance (avoiding per-key allocations), consider documenting the rationale. Otherwise, a more flexible signature could improve ergonomics:

async fn get_multi<K: AsRef<[u8]> + Send>(
    &self,
    keys: &[K],
) -> KvResult<Vec<Option<Bytes>>>;
benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md (1)

260-260: Minor style suggestion: Add comma after year in date.

Some style guides suggest commas after the year in month-day-year format. Currently "December 13, 2025" could be "December 13, 2025," (comma before or after depending on guide).

This is purely stylistic and can be deferred.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f601479 and 8204c5c.

⛔ Files ignored due to path filters (1)
  • benches/d-engine-bench/reports/v0.2.0/dengine_comparison_v0.2.0.png is excluded by !**/*.png
📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • MIGRATION_GUIDE.md (1 hunks)
  • README.md (4 hunks)
  • benches/d-engine-bench/reports/v0.1.4/report_20251205_remeasured.md (1 hunks)
  • benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md (1 hunks)
  • d-engine-client/src/kv_client.rs (1 hunks)
🧰 Additional context used
🪛 LanguageTool
benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md

[style] ~260-~260: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ... v0.2.0 Report Date: December 13, 2025 Test Environment: Apple M2 Mac mi...

(MISSING_COMMA_AFTER_YEAR)

benches/d-engine-bench/reports/v0.1.4/report_20251205_remeasured.md

[style] ~3-~3: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...enchmark Report Date: December 13, 2025 Hardware: Apple Mac mini M2 (8-co...

(MISSING_COMMA_AFTER_YEAR)


[style] ~185-~185: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...ne v0.1.4 Test Date: December 13, 2025 Benchmark Runs: Single run per co...

(MISSING_COMMA_AFTER_YEAR)

🪛 markdownlint-cli2 (0.18.1)
MIGRATION_GUIDE.md

71-71: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ 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: audit
  • GitHub Check: Analyze (rust)
  • GitHub Check: audit
🔇 Additional comments (12)
benches/d-engine-bench/reports/v0.1.4/report_20251205_remeasured.md (1)

1-187: Excellent benchmark documentation structure.

This remeasured baseline report is well-organized and provides clear, reproducible performance data for v0.1.4. The sections are logically arranged, test commands are fully specified, and the comparison with the original report includes reasonable context for differences. This provides a solid foundation for comparing v0.2.0 performance.

d-engine-client/src/kv_client.rs (4)

32-52: Well-designed trait definition.

Good use of async_trait with explicit Send + Sync bounds. The documentation clearly explains consistency guarantees and performance characteristics for each implementation.


74-78: LGTM!

The generic bounds impl AsRef<[u8]> + Send provide good flexibility for callers while maintaining type safety.


136-139: LGTM!

Consistent API design with clear semantics for missing/expired keys returning None.


173-197: LGTM!

Idempotent delete semantics are appropriate for a distributed KV store, avoiding race conditions and simplifying retry logic.

MIGRATION_GUIDE.md (1)

42-90: Migration strategies are clear and well-documented.

The three migration options (Clean Start, Rolling Upgrade, Snapshot-based) are practical, actionable, and appropriately scoped for different deployment scenarios. The verification section and timeline are helpful for operators. No concerns.

CHANGELOG.md (1)

7-86: v0.2.0 release entry is comprehensive and well-structured.

The new release notes clearly articulate feature value, provide actionable examples, explicitly call out breaking changes with migration guidance, and back performance claims with benchmarks. Structure and tone are professional. No concerns.

benches/d-engine-bench/reports/v0.2.0/report_v0.2.0_final.md (2)

3-20: TL;DR is clear and provides honest hardware context.

The executive summary effectively communicates v0.2.0 advantages while explicitly noting hardware differences between test setups. This transparency is important for readers evaluating the results. Good practice.


86-134: Reproduction steps are detailed and executable.

The benchmark commands include all necessary flags (endpoints, connections, clients, key/value sizes, consistency levels) for reproducibility. Commands span single-client, high-concurrency, and hot-key scenarios. Clear structure. No concerns.

README.md (3)

37-53: v0.2.0 features and core capabilities are well-articulated.

The distinction between new features (Modular Workspace, TTL/Lease, Watch API, etc.) and core capabilities (Single-Node Start, Strong Consistency, Pluggable Storage) is clear and helpful. Feature descriptions are concise with concrete examples (e.g., "<0.1ms latency" for LocalKvClient).


15-31: Project Status section sets proper expectations for pre-1.0 users.

Clearly states that breaking changes may occur between minor versions, explicitly references MIGRATION_GUIDE.md for compatibility promise, and provides timeline context (v1.0 expected Q2 2026). This is valuable transparency for production users. Approach is professional and encourages adoption while managing expectations.


57-116: Quick Start reorganization is significantly improved.

Splitting installation guidance into three distinct modes (Embedded/Standalone/Custom Storage) with use cases, TOML examples, and "when to use" guidance resolves the ambiguity that likely existed in v0.1.x. Each option includes:

  • Clear TOML syntax for each mode
  • Use case examples (e.g., "Go/Python/Java apps for Standalone")
  • Performance note for context
  • Link to relevant documentation

This structure is intuitive for both new users and experienced operators choosing deployment architecture.

Comment thread MIGRATION_GUIDE.md Outdated
Comment thread README.md
…ments

This PR consolidates architecture review improvements and critical bug fixes for v0.2.0 release.

## 🎯 Critical Fixes

- **#212**: Fix learner promotion stuck (voter count + role transition bugs)
- **#218**: Fix leader next_index initialization for new learners
- **#222**: Return NOT_LEADER with leader metadata for client redirection
- **#209**: Fix node restart wait_ready() timeout (leader notification race)

## ✨ Feature Additions

- **#213**: Implement READ_ONLY Learner nodes for permanent analytics
- **#219**: Pre-generate Go protobuf code for zero-config experience

## 🔧 Refactoring & Performance

- **#223**: Optimize check_learner_progress() lock contention
  - Extract 5 helper methods (SRP), add 11 unit tests
- **#211**: Remove Arc::get_mut anti-pattern from lease injection
- **#210**: Simplify watch architecture (tokio::broadcast, 90% code sharing)
- **#217**: Refactor Node::run() with strategy pattern
- **#209**: Consolidate Raft unit tests (migrate 27 tests from server to core)

## 📚 Documentation

- Restructure quick-start docs (embedded + standalone examples)
- Add integration-modes.md and use-cases.md
- Delete 911 lines of internal architecture docs (20/80 principle)
- Fix Go client example with pre-generated protobufs

## 🧪 Testing & Validation

- 14+ new unit tests across components
- Fix flaky tests and timing issues (#204, #209)
- Optimize test suite with nextest
- All 430 core + 305 server + 292 integration tests passing

## 📊 Performance

- Benchmarks stable (±3% variance)
- CI optimized: removed benchmark compile check (saves 2-3 min/run)

---

**Closes:** #209, #210, #211, #212, #213, #217, #218, #219, #222, #223

**Migration Notes:**
- Config: `raft.watch.enabled` removed (breaking)
- API: `StateMachine::start()` changed to async (breaking)
- NodeStatus enum refactored: PROMOTABLE/READ_ONLY/ACTIVE (non-breaking)

**Files Changed:** 100+ files, ~5000 insertions, ~1500 deletions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants