Skip to content

perf #392: ReadActor fast path for Lease/Eventual Read — fix v0.2.4 throughput regression#401

Merged
JoshuaChi merged 12 commits into
mainfrom
perf/392-read
Jun 7, 2026
Merged

perf #392: ReadActor fast path for Lease/Eventual Read — fix v0.2.4 throughput regression#401
JoshuaChi merged 12 commits into
mainfrom
perf/392-read

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented May 30, 2026

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Introduces a dedicated ReadActor that serves LeaseRead and EventualConsistency reads directly from the state machine, bypassing the Raft command channel. Also fixes a positional alignment bug in read_handle.rs where sparse KvEntry results were not re-aligned to the request key order.

Type:

  • Bug Fix — sparse KvEntry misalignment in read_handle.rs
  • Feature — StateMachine::get_multi added to trait
  • Performance — ReadActor eliminates cmd_tx contention for Lease/Eventual reads
  • Test/Coverage

Why Is This Needed?

Closes #392.

v0.2.4 routed all read policies through cmd_tx (the Raft loop), causing channel contention under high concurrency.

LeaseRead and EventualConsistency do not require consensus — they only need a valid lease check or a running state machine. This PR adds a dedicated ReadActor channel that short-circuits directly to the state machine. The Raft loop is untouched for writes and LinearizableRead.


Checklist

Required:

  • make test passes
  • Added tests for new code
  • Commits squashed to 1-2 logical units

If changing APIs:

  • Updated relevant docs
  • Explained why complexity is justified

Testing

  • ReadActor (9 tests), ReadHandle alignment (13 tests), ReadLease::revoke / is_valid (10 tests)
  • StateMachine::get_multi RocksDB + FileStateMachine snapshot isolation (8 tests each)
  • gRPC batch fast path (8 tests)
  • become_follower lease revocation — 7 tests including 3 window-period tests that verify lease.revoke() fires at higher-term detection, not only at role transition
  • Config validation: ReadActorConfig (4 tests), ReadConsistencyConfig lease safety (1 unit test directly on validate(election_timeout_min))

Benchmark (3-node embedded, same machine):

Config LeaseRead EventualRead vs v0.2.4
cap=1024 ~790K/s ~806K/s +8% / +7%
cap=10240 ~810K/s ~851K/s +11% / +13%

Architecture

EmbeddedClient / gRPC Node
  ├── read_tx  ──►  ReadActor  ──►  SM.get_multi()   (Eventual / Lease)
  └── cmd_tx   ──►  Raft loop                         (Linearizable / Write)

Reviewer Notes

  1. Lease revocation window (safety-critical)revoke() is now called at the moment higher-term is detected in VoteRequest / AppendEntries / AppendResult handlers, before send_become_follower_event(). The previous impl only called revoke() inside become_follower(), leaving a window where ReadActor could observe a valid lease between event emission and Raft loop processing. All 3 detection paths have tests pinning this invariant.

  2. read_handle.rs alignment fixread_from_state_machine returns only existing keys (sparse). Public API contract requires result[i] ↔ keys[i]. Fix uses a HashMap at the ReadHandle boundary. 5 positional tests cover all cases (first/middle/last missing, all-missing, all-present).

  3. StateMachine::get_multi — added to trait with correct-but-sequential default impl. RocksDB and FileStateMachine override with snapshot isolation. Zero breaking change for existing SM implementors.

  4. BatchRead trait deleted — was pub(crate), internal only. Replaced by get_multi on the public trait.

  5. ReadConsistencyConfig::validate — lease safety constraint (lease_duration_ms < election_timeout_min) moved from RaftConfig::validate() into ReadConsistencyConfig::validate(election_timeout_min), making the invariant self-contained at the right layer.


Estimated review complexity:

  • Quick (< 100 lines)
  • Medium (< 300 lines)
  • Deep (> 300 lines) — 27 files, ~2500 lines net

Summary by CodeRabbit

  • New Features
    • Read-actor fast path for Eventual/Lease reads; batch multi-key read API; DefaultEmbeddedEngine/DefaultEmbeddedClient for embedded mode.
  • Changes
    • TTL expiration always active (legacy toggle removed); new read-actor tuning params (channel_capacity, max_drain); storage close hook on shutdown.
  • Improvements
    • Lower-latency eventual/lease reads, read-path tuning guidance, expanded benchmark report (v0.2.5).
  • Bug Fixes
    • Storage lock released immediately on stop; stability and correctness fixes around lease revocation.

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7d63c9c-7ce8-4c8e-b0a2-1827c0b1c156

📥 Commits

Reviewing files that changed from the base of the PR and between f2db99a and 64b20df.

📒 Files selected for processing (2)
  • d-engine-core/src/config/raft.rs
  • d-engine-core/src/config/raft_test.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • d-engine-core/src/config/raft.rs

📝 Walkthrough

Walkthrough

Implements a ReadActor fast path with atomic ReadLease and monotonic clock; adds StateMachine batch read (get_multi) and close_storage; migrates leases to TtlLease; refactors embedded engine/client types and read handles; adds RocksDB hard-close; wires gRPC/standalone fast path; updates tests, benches, configs, and docs.

Changes

v0.2.5 read fast-path & storage lifecycle

Layer / File(s) Summary
Release notes, migration guide, benches, examples
CHANGELOG.md, MIGRATION_GUIDE.md, benches/reports/*, benches/*, examples/*, README.md
Adds v0.2.5 notes and migration instructions; new bench report and example updates; many examples/benches/tests now use DefaultEmbeddedEngine and include [raft.read_actor] snippets.
Config contracts & validation
config/base/raft.toml, d-engine-core/src/config/raft.rs, d-engine-core/src/config/raft_test.rs
Adds ReadActorConfig (channel_capacity, max_drain) and network_rtt_p99_ms; ReadConsistencyConfig::validate tightened to require lease_duration_ms + rtt/2 < election_timeout_min.
ReadLease & monotonic clock
d-engine-core/src/raft_role/read_lease.rs, d-engine-core/src/raft_role/mod.rs, read_lease tests
Adds monotonic now_ms/init_clock and a lock-free ReadLease packing term+deadline with renew/invalidate/revoke and read-side is_valid APIs; SharedState exposes Arc<ReadLease>.
Leader lease handling
d-engine-core/src/raft_role/leader_state.rs, leader tests
Leader uses last_heartbeat_send_ts and anchors renewals to send time; immediate lease.revoke() on step-down; is_lease_valid() signature simplified; tests updated/added for send-ts invariants and early revocation.
StateMachine contract
d-engine-core/src/storage/state_machine.rs, state machine tests
Adds get_multi(&[Bytes]) -> Vec<Option<Bytes>> and close_storage() hook (no-op default), with tests verifying positional ordering and snapshot coherence.
ReadActor task & Node wiring
d-engine-server/src/read_actor.rs, d-engine-server/src/read_actor_test.rs, d-engine-server/src/node/builder.rs, d-engine-server/src/node/mod.rs
Introduces run_read_actor task serving Eventual/Lease reads via sm.get_multi; defines ReadCmd and ReadActorError; Node builder spawns/aborts actor and stores read_handle, read_actor_handle, and read_lease.
Embedded/Standalone read handles & client refactor
d-engine-server/src/api/embedded_read_handle.rs, d-engine-server/src/api/standalone_read_handle.rs, d-engine-server/src/api/embedded_client.rs, d-engine-server/src/api/embedded.rs, d-engine-server/src/api/mod.rs
Adds EmbeddedReadHandle and StandaloneReadHandle to select SM fast-path vs cmd_tx fallback; refactors EmbeddedEngine/EmbeddedClient to generics and adds DefaultEmbedded* aliases; stop() calls close_storage().
gRPC fast-path routing & proto helper
d-engine-server/src/network/grpc/grpc_raft_service.rs, d-engine-server/src/proto_convert.rs, d-engine-server/src/network/grpc/grpc_raft_service_fast_path_test.rs
gRPC handle_client_read routes Eventual/Lease reads to read_handle.get_batch(...), maps errors to gRPC Status, and includes tests validating fast-path vs fallback behavior.
TtlLease migration & RocksDB lifecycle
d-engine-server/src/storage/lease.rs, .../file_state_machine.rs, .../rocksdb_state_machine.rs, storage tests
Replaces DefaultLease with TtlLease; adds get_multi and close_storage implementations; refactors RocksDB handle to ArcSwapOption for atomic close_db() and immediate LOCK release; adds snapshot-consistency multi-key tests.
Tests, mocks, helpers
test_utils/*, d-engine-server/tests/*, many unit/integration tests
Wires read_lease/read_handle into mocks and tests; updates many tests to use DefaultEmbeddedEngine; adds wide coverage for read fast path, lease revocation, and storage coherence.

Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"I’m a rabbit, I hop the read trail,
Atomic lease in paw, I never fail.
ReadActor drains queues with joyful cheer,
TtlLease keeps TTL tidy and clear.
Benchmarks hum — zero RTT, hooray! 🐇"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/392-read

@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: 4

🧹 Nitpick comments (1)
d-engine-server/src/storage/lease.rs (1)

1-1: 💤 Low value

Stale doc references to "Default lease" after rename.

The module doc (Line 1) and the struct doc (Line 44) still describe this as the "Default lease implementation", but the type is now TtlLease. Update the wording to avoid confusion for API consumers.

📝 Suggested doc wording update
-//! Default lease implementation for d-engine.
+//! TTL lease implementation for d-engine.
-/// Default lease implementation with single-index lock-free architecture.
+/// TTL lease implementation with single-index lock-free architecture.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-server/src/storage/lease.rs` at line 1, Update the stale
documentation wording that refers to "Default lease implementation" to reference
the renamed type TtlLease: change the module-level doc (top of the file) and the
struct doc for TtlLease (previously describing "Default lease") so they describe
this as the TTL-based lease implementation (or "TtlLease") to match the exported
type name and avoid confusion for API consumers; search for the text "Default
lease" in this file and replace it with wording that mentions TtlLease/TTL-based
lease in both the module doc and the doc comment above the TtlLease struct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@d-engine-core/src/config/raft.rs`:
- Around line 229-267: Add runtime validation for the ReadActor knobs by
implementing ReadActorConfig::validate() that rejects channel_capacity == 0
(since tokio mpsc panics on 0) and rejects max_drain == 0 (and optionally
enforce max_drain <= channel_capacity), returning an error or Result to indicate
invalid config; then call ReadActorConfig::validate() from
RaftConfig::validate() so startup fails fast with a clear error message
referencing the offending field(s) (use the struct names channel_capacity and
max_drain in the error text to aid diagnostics).

In `@d-engine-core/src/raft_role/leader_state.rs`:
- Around line 700-701: The code only calls self.shared_state.lease.revoke()
inside the become_follower() transition, leaving a window where ReadActor can
still serve stale LeaseRead; update the higher-term handling branches (the
VoteRequest, AppendEntries, and AppendResult branches where you detect a higher
term) to call self.shared_state.lease.revoke() immediately when leadership is
known lost, before publishing BecomeFollower() and before any early returns, so
the lease is invalidated as soon as a higher-term is observed and ReadActor
cannot serve stale reads.

In `@d-engine-core/src/raft_role/read_lease.rs`:
- Around line 55-64: The pack function currently uses debug_assert! so deadline
overflow is silently truncated in release builds; replace the debug-only check
with a runtime validation: in fn pack(term: u64, deadline_ms: u64) -> u64 check
if deadline_ms <= Self::DEADLINE_MASK and if not panic with a clear message
(e.g. assert!(... , "deadline_ms overflows 48 bits")), ensuring the invariant
established by renew() cannot be violated in release builds; update any tests or
callers expecting previous behavior if necessary.

In `@d-engine-server/src/proto_convert.rs`:
- Around line 214-215: The doc comment above the function in proto_convert.rs is
contradictory: the first sentence says "single-key fast-path read" while the
next says "batch of keys" and the function signature accepts &[bytes::Bytes]
(multiple keys); update the documentation for the function (the comment block
above the function that builds a successful ClientResponse proto/fast-path read)
to consistently describe it as building a fast-path read response for a batch of
keys (or phrase it generically for multiple keys), and remove or replace the
"single-key" wording so the comment matches the &[bytes::Bytes] parameter and
the function's actual behavior.

---

Nitpick comments:
In `@d-engine-server/src/storage/lease.rs`:
- Line 1: Update the stale documentation wording that refers to "Default lease
implementation" to reference the renamed type TtlLease: change the module-level
doc (top of the file) and the struct doc for TtlLease (previously describing
"Default lease") so they describe this as the TTL-based lease implementation (or
"TtlLease") to match the exported type name and avoid confusion for API
consumers; search for the text "Default lease" in this file and replace it with
wording that mentions TtlLease/TTL-based lease in both the module doc and the
doc comment above the TtlLease struct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d5c766c-424a-4c23-a6c0-3f5aa06acb59

📥 Commits

Reviewing files that changed from the base of the PR and between f8f4a9d and 2d1245d.

📒 Files selected for processing (86)
  • CHANGELOG.md
  • MIGRATION_GUIDE.md
  • benches/embedded-bench/config/n1.toml
  • benches/embedded-bench/config/n2.toml
  • benches/embedded-bench/config/n3.toml
  • benches/embedded-bench/src/main.rs
  • benches/reports/v0.2.5/bench_report_v0.2.5.md
  • config/base/raft.toml
  • d-engine-core/src/config/raft.rs
  • d-engine-core/src/raft.rs
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-core/src/raft_role/leader_state_test/become_follower_test.rs
  • d-engine-core/src/raft_role/leader_state_test/client_read_test.rs
  • d-engine-core/src/raft_role/leader_state_test/lease_refresh_on_log_flushed_test.rs
  • d-engine-core/src/raft_role/leader_state_test/mod.rs
  • d-engine-core/src/raft_role/leader_state_test/pending_lease_reads_test.rs
  • d-engine-core/src/raft_role/mod.rs
  • d-engine-core/src/raft_role/read_lease.rs
  • d-engine-core/src/raft_role/read_lease_test.rs
  • d-engine-core/src/storage/state_machine.rs
  • d-engine-core/src/storage/state_machine_test.rs
  • d-engine-server/benches/lease_performance.rs
  • d-engine-server/benches/state_machine.rs
  • d-engine-server/benches/ttl.rs
  • d-engine-server/benches/watch_overhead.rs
  • d-engine-server/src/api/embedded.rs
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/api/embedded_client_fast_path_test.rs
  • d-engine-server/src/api/embedded_client_test.rs
  • d-engine-server/src/api/embedded_test.rs
  • d-engine-server/src/api/mod.rs
  • d-engine-server/src/api/read_handle.rs
  • d-engine-server/src/api/read_handle_test.rs
  • d-engine-server/src/api/standalone.rs
  • d-engine-server/src/lib.rs
  • d-engine-server/src/membership/membership_snapshot.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-server/src/network/grpc/grpc_raft_service_fast_path_test.rs
  • d-engine-server/src/network/grpc/mod.rs
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/node/mod.rs
  • d-engine-server/src/proto_convert.rs
  • d-engine-server/src/read_actor.rs
  • d-engine-server/src/read_actor_test.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine_test.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_unified_engine_test.rs
  • d-engine-server/src/storage/lease.rs
  • d-engine-server/src/storage/lease_integration_test.rs
  • d-engine-server/src/storage/lease_unit_test.rs
  • d-engine-server/src/storage/mod.rs
  • d-engine-server/src/test_utils/mock/mock_node_builder.rs
  • d-engine-server/tests/cas_operations/bank_transfer_invariant_embedded.rs
  • d-engine-server/tests/cas_operations/distributed_lock_embedded.rs
  • d-engine-server/tests/cas_operations/leader_failover_cas_embedded.rs
  • d-engine-server/tests/cas_operations/snapshot_recovery_embedded.rs
  • d-engine-server/tests/cluster_lifecycle/config_loading_embedded.rs
  • d-engine-server/tests/cluster_lifecycle/scale_single_to_three_node_embedded.rs
  • d-engine-server/tests/cluster_lifecycle/single_node_bootstrap_embedded.rs
  • d-engine-server/tests/cluster_state_and_metadata/cluster_state_consensus_embedded.rs
  • d-engine-server/tests/consistent_reads/lease_read_embedded.rs
  • d-engine-server/tests/consistent_reads/linearizable_read_batching_embedded.rs
  • d-engine-server/tests/consistent_reads/linearizable_read_consistency_embedded.rs
  • d-engine-server/tests/drain_batching/select_fairness_embedded.rs
  • d-engine-server/tests/embedded_client/embedded_client_operations.rs
  • d-engine-server/tests/failover_and_recovery/leader_failover_embedded.rs
  • d-engine-server/tests/failover_and_recovery/mod.rs
  • d-engine-server/tests/readonly_and_learner_mode/learner_readonly_sync_embedded.rs
  • d-engine-server/tests/readonly_and_learner_mode/learner_readonly_sync_standalone.rs
  • d-engine-server/tests/snapshot_and_recovery/snapshot_concurrent_replication_embedded.rs
  • d-engine-server/tests/snapshot_and_recovery/snapshot_concurrent_writes_embedded.rs
  • d-engine-server/tests/snapshot_and_recovery/snapshot_follower_generation_embedded.rs
  • d-engine-server/tests/snapshot_and_recovery/snapshot_interrupted_transfer_embedded.rs
  • d-engine-server/tests/snapshot_and_recovery/snapshot_leader_change_during_transfer_embedded.rs
  • d-engine-server/tests/watch_and_subscriptions/watch_events_embedded.rs
  • d-engine-server/tests/watch_and_subscriptions/watch_membership_embedded.rs
  • d-engine-server/tests/watch_and_subscriptions/watch_performance_gate_embedded.rs
  • d-engine/src/lib.rs
  • examples/quick-start-embedded/src/main.rs
  • examples/service-discovery-embedded/server.rs
  • examples/three-nodes-embedded/src/main.rs
  • examples/three-nodes-standalone/config/n1.toml
  • examples/three-nodes-standalone/config/n2.toml
  • examples/three-nodes-standalone/config/n3.toml

Comment thread d-engine-core/src/config/raft.rs
Comment thread d-engine-core/src/raft_role/leader_state.rs
Comment thread d-engine-core/src/raft_role/read_lease.rs
Comment thread d-engine-server/src/proto_convert.rs Outdated
JoshuaChi added 3 commits May 31, 2026 11:39
…eadActor fast path for Eventual/LeaseRead

- read_handle.rs: positional alignment bug — sparse KvEntry from
  read_from_state_machine was not re-aligned to keys[i]; fix with
  HashMap to guarantee result[i] ↔ keys[i] regardless of SM response
- Delete BatchRead trait (was pub(crate)); replace with
  StateMachine::get_multi (default sequential impl + RocksDB /
  FileStateMachine snapshot-isolated overrides)
- Move ReadActor to d-engine-server with TypeConfig generic;
  wire ReadHandle for gRPC and EmbeddedClient multi-key fast path
- Fix expect_get → expect_get_multi in 3 test files
- Add 5 alignment tests + 11 gRPC fast path tests
…ation

- leader_state: revoke lease immediately on higher-term detection
  (VoteRequest / AppendEntries / AppendResult) — eliminates the window
  between send_become_follower_event() and become_follower() where
  ReadActor could still serve stale LeaseRead
- read_lease: promote debug_assert! to assert! so overflow guard fires
  in release builds
- ReadActorConfig: add validate() rejecting channel_capacity=0 and
  max_drain=0; call from RaftConfig::validate()
- ReadConsistencyConfig: move lease_duration_ms < election_timeout_min
  constraint into ReadConsistencyConfig::validate(election_timeout_min),
  making the safety invariant self-contained at the right layer
- proto_convert: remove contradictory "single-key" doc comment
- Tests: 3 window-period tests for become_follower, 4 ReadActorConfig
  validation tests, 1 ReadConsistencyConfig lease safety unit test
… path

- Replace EmbeddedEngine/EmbeddedClient with DefaultEmbeddedEngine/
  DefaultEmbeddedClient in all code examples (README, quick-start,
  integration-modes, watch-feature, ha-deployment, d-engine-server
  README, examples/quick-start-embedded)
- read-consistency: document ReadActor fast path, fix EventualConsistency
  "Any node" claim, add 0 RTT explanation
- consistency-tuning: fix lease_duration_ms default (500→250), fix all
  scenario configs that would fail validate(), add both formula bounds,
  remove 6 non-existent raft.xxx metrics, drop fictional Monitoring section
- throughput-optimization-guide: replace architectural prose with
  actionable channel_capacity/max_drain tuning table (correct defaults:
  capacity=512, max_drain=100)
- raft.rs lease_duration_ms: add full safety constraint doc including
  theoretical bound and clock drift guidance; fix wrong default comment

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@d-engine-server/src/api/read_handle.rs`:
- Around line 181-201: The fast-path read ignores the caller's timeout because
reply_rx.await is unbounded; wrap the oneshot receive in
tokio::time::timeout(timeout, reply_rx).await and treat a timeout as a
fallthrough to the cmd_tx path. Concretely, replace reply_rx.await match with a
timeout call and then match the timeout result: on Ok(Ok(Ok(values))) return
Ok(values); on Ok(Ok(Err(ReadActorError::SmError(e)))) return
Err(server_error(e)); on Ok(Ok(Err(ReadActorError::LeaseInvalid |
ReadActorError::SmStopped))) fall through; on Ok(Err(_)) (oneshot dropped) fall
through; on Err(_) (timeout elapsed) fall through to cmd_tx. Keep existing
send/read_tx and ReadCmd logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d09c68d-bafe-4b14-a3e5-63c068a555e0

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1245d and c2f256e.

📒 Files selected for processing (41)
  • README.md
  • d-engine-core/src/config/raft.rs
  • d-engine-core/src/config/raft_test.rs
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-core/src/raft_role/leader_state_test/become_follower_test.rs
  • d-engine-core/src/raft_role/read_lease.rs
  • d-engine-core/src/storage/state_machine.rs
  • d-engine-core/src/storage/state_machine_test.rs
  • d-engine-server/README.md
  • d-engine-server/src/api/embedded.rs
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/api/embedded_client_fast_path_test.rs
  • d-engine-server/src/api/embedded_test.rs
  • d-engine-server/src/api/mod.rs
  • d-engine-server/src/api/read_handle.rs
  • d-engine-server/src/api/read_handle_test.rs
  • d-engine-server/src/lib.rs
  • d-engine-server/src/membership/membership_snapshot.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-server/src/network/grpc/grpc_raft_service_fast_path_test.rs
  • d-engine-server/src/network/grpc/mod.rs
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/node/mod.rs
  • d-engine-server/src/proto_convert.rs
  • d-engine-server/src/read_actor.rs
  • d-engine-server/src/read_actor_test.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine_test.rs
  • d-engine-server/src/test_utils/mock/mock_node_builder.rs
  • d-engine/src/docs/client_guide/read-consistency.md
  • d-engine/src/docs/examples/ha-deployment-load-balancing.md
  • d-engine/src/docs/integration-modes.md
  • d-engine/src/docs/overview.md
  • d-engine/src/docs/performance/throughput-optimization-guide.md
  • d-engine/src/docs/quick-start-5min.md
  • d-engine/src/docs/quick-start-standalone.md
  • d-engine/src/docs/server_guide/consistency-tuning.md
  • d-engine/src/docs/server_guide/watch-feature.md
  • examples/quick-start-embedded/README.md
✅ Files skipped from review due to trivial changes (6)
  • README.md
  • examples/quick-start-embedded/README.md
  • d-engine/src/docs/quick-start-standalone.md
  • d-engine-server/src/network/grpc/mod.rs
  • d-engine/src/docs/server_guide/watch-feature.md
  • d-engine-server/src/membership/membership_snapshot.rs
🚧 Files skipped from review as they are similar to previous changes (20)
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine_test.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs
  • d-engine-core/src/storage/state_machine.rs
  • d-engine-server/src/proto_convert.rs
  • d-engine-server/src/lib.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-server/src/test_utils/mock/mock_node_builder.rs
  • d-engine-server/src/api/mod.rs
  • d-engine-core/src/storage/state_machine_test.rs
  • d-engine-server/src/node/mod.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine.rs
  • d-engine-server/src/api/embedded_test.rs
  • d-engine-server/src/read_actor.rs
  • d-engine-server/src/api/read_handle_test.rs
  • d-engine-server/src/read_actor_test.rs
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-server/src/api/embedded.rs

Comment thread d-engine-server/src/api/standalone_read_handle.rs
…for embedded mode

Split ReadHandle into StandaloneReadHandle (gRPC/ReadActor path) and
EmbeddedReadHandle<T> (direct SM call, zero oneshot/channel overhead).
EmbeddedClient now uses EmbeddedReadHandle<T>, recovering v0.2.3 latency.
Add close_storage() to StateMachine trait for deterministic LOCK release on stop().

@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.

🧹 Nitpick comments (2)
d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs (1)

89-89: 💤 Low value

Remove redundant .to_path_buf() calls.

Several test setups call .to_path_buf().to_path_buf() which unnecessarily clones the PathBuf twice. The first to_path_buf() already returns an owned PathBuf.

♻️ Suggested simplification

For example, line 89:

-    let data_dir = temp_dir.path().to_path_buf().to_path_buf();
+    let data_dir = temp_dir.path().to_path_buf();

Apply the same fix to the other occurrences at lines 165, 219-221, 255, 309, 356-358, 408-410, 447-449, and 538-540.

Also applies to: 165-165, 219-221, 255-255, 309-309, 356-358, 408-410, 447-449, 538-540

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs` at line
89, The test code creates data_dir by calling
temp_dir.path().to_path_buf().to_path_buf(), which redundantly clones the
PathBuf; change assignments that use temp_dir.path().to_path_buf().to_path_buf()
(e.g., the data_dir variable and other occurrences noted) to a single
temp_dir.path().to_path_buf() so you only convert the Path to an owned PathBuf
once, updating all instances that use the same pattern (search for
temp_dir.path().to_path_buf().to_path_buf() and replace with
temp_dir.path().to_path_buf()).
d-engine-server/src/api/embedded_read_handle.rs (1)

113-122: 💤 Low value

Unreachable comment inside return block.

The comment on line 119 // SM stopped or hard error → fall through is placed after the return Ok(values); statement inside the if let block, making it unreachable code. The logic is correct, but the comment should be placed after the closing brace of the inner if let or merged with the comment on line 121.

This is purely cosmetic and doesn't affect correctness.

♻️ Proposed fix for comment placement
             ReadConsistencyPolicy::LeaseRead => {
                 if self.lease.is_valid(now_ms())
                     && let Ok(values) = self.sm.get_multi(keys)
                 {
                     return Ok(values);
-
-                    // SM stopped or hard error → fall through
                 }
-                // invalid lease → fall through to cmd_tx
+                // invalid lease or SM stopped/error → fall through to cmd_tx
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-server/src/api/embedded_read_handle.rs` around lines 113 - 122, The
comment after the return in the ReadConsistencyPolicy::LeaseRead branch is
unreachable; move the note about "SM stopped or hard error → fall through" so it
sits after the closing brace of the inner if-let (the block that checks
self.lease.is_valid(now_ms()) && let Ok(values) = self.sm.get_multi(keys)) or
merge it with the adjacent "invalid lease → fall through to cmd_tx" comment,
ensuring the comment refers to the outcome of self.sm.get_multi and not placed
after the return Ok(values).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@d-engine-server/src/api/embedded_read_handle.rs`:
- Around line 113-122: The comment after the return in the
ReadConsistencyPolicy::LeaseRead branch is unreachable; move the note about "SM
stopped or hard error → fall through" so it sits after the closing brace of the
inner if-let (the block that checks self.lease.is_valid(now_ms()) && let
Ok(values) = self.sm.get_multi(keys)) or merge it with the adjacent "invalid
lease → fall through to cmd_tx" comment, ensuring the comment refers to the
outcome of self.sm.get_multi and not placed after the return Ok(values).

In `@d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs`:
- Line 89: The test code creates data_dir by calling
temp_dir.path().to_path_buf().to_path_buf(), which redundantly clones the
PathBuf; change assignments that use temp_dir.path().to_path_buf().to_path_buf()
(e.g., the data_dir variable and other occurrences noted) to a single
temp_dir.path().to_path_buf() so you only convert the Path to an owned PathBuf
once, updating all instances that use the same pattern (search for
temp_dir.path().to_path_buf().to_path_buf() and replace with
temp_dir.path().to_path_buf()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95f2e5e1-6120-45ed-8235-3a601a794dc6

📥 Commits

Reviewing files that changed from the base of the PR and between c2f256e and 6691249.

📒 Files selected for processing (27)
  • benches/embedded-bench/config/n1.toml
  • benches/embedded-bench/config/n2.toml
  • benches/embedded-bench/config/n3.toml
  • d-engine-core/src/storage/state_machine.rs
  • d-engine-server/src/api/embedded.rs
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/api/embedded_client_test/embedded_client_test.rs
  • d-engine-server/src/api/embedded_client_test/mod.rs
  • d-engine-server/src/api/embedded_read_handle.rs
  • d-engine-server/src/api/embedded_read_handle_test.rs
  • d-engine-server/src/api/embedded_test/embedded_env_test.rs
  • d-engine-server/src/api/embedded_test/embedded_test.rs
  • d-engine-server/src/api/embedded_test/mod.rs
  • d-engine-server/src/api/mod.rs
  • d-engine-server/src/api/standalone.rs
  • d-engine-server/src/api/standalone_read_handle.rs
  • d-engine-server/src/api/standalone_read_handle_test.rs
  • d-engine-server/src/network/grpc/grpc_raft_service_fast_path_test.rs
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/node/mod.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine.rs
  • d-engine-server/src/storage/adaptors/file/file_state_machine_test.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine_test.rs
  • d-engine-server/src/test_utils/mock/mock_node_builder.rs
  • d-engine/src/docs/server_guide/customize-state-machine.md
  • examples/sled-cluster/src/sled_state_machine.rs
💤 Files with no reviewable changes (1)
  • d-engine-server/src/api/embedded_test/embedded_test.rs
✅ Files skipped from review due to trivial changes (3)
  • d-engine-server/src/api/embedded_test/mod.rs
  • benches/embedded-bench/config/n1.toml
  • d-engine-server/src/api/embedded_client_test/mod.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • benches/embedded-bench/config/n3.toml
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/api/standalone.rs
  • d-engine-server/src/test_utils/mock/mock_node_builder.rs
  • d-engine-server/src/network/grpc/grpc_raft_service_fast_path_test.rs
  • d-engine-server/src/node/mod.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine_test.rs

JoshuaChi added 2 commits June 1, 2026 21:17
…mbler test

- StandaloneReadHandle fast path: wrap reply_rx.await with
  tokio::time::timeout(timeout, ...) so a stalled ReadActor falls through
  to cmd_tx instead of blocking the caller indefinitely
- Add test_read_actor_stall_respects_timeout to verify the timeout fires
  within the caller-supplied deadline
- snapshot_assembler_test: call flush_to_disk() before drop in first
  transfer block; tokio::fs::File buffers writes internally and drop does
  not guarantee OS flush, causing non-deterministic stale file size on CI

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benches/reports/v0.2.5/bench_report_v0.2.5.md`:
- Around line 83-85: Update the summary sentence "v0.2.5 Standalone is stable vs
v0.2.4" to remove the blanket "all write and read scenarios within ±2%" claim
and instead scope it to exclude the Lease Read result (which is +10.8% vs
v0.2.4). Specifically, edit the line containing the phrase "v0.2.5 Standalone is
stable vs v0.2.4" so it either states "all write and read scenarios except Lease
Read are within ±2%" or otherwise calls out Lease Read separately by name,
ensuring the Lease Read +10.8% figure is not contradicted.
- Line 130: The report line incorrectly attributes the +5.7% Linearizable Read
improvement to a “Read-index optimization in ReadActor” despite the PR leaving
Linearizable reads and the Raft loop unchanged; edit the sentence referencing
ReadActor/Linearizable reads to remove causal language and present it as an
observed benchmark delta only (e.g., “Linearizable Read +5.7% vs v0.2.4 —
observed delta; no Linearizable-path code/raft loop changes in this PR”),
ensuring mentions of ReadActor, Linearizable reads, and the Raft loop remain for
context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3427a57-e52f-4c0f-9db9-78b1e7902ba0

📥 Commits

Reviewing files that changed from the base of the PR and between ccaecec and f45293e.

⛔ Files ignored due to path filters (3)
  • benches/reports/v0.2.5/d-engine_comparison_v0.2.5.png is excluded by !**/*.png
  • benches/reports/v0.2.5/d-engine_v0.2.5_vs_v0.2.4_embedded_mode.png is excluded by !**/*.png
  • benches/reports/v0.2.5/d-engine_v0.2.5_vs_v0.2.4_standalone_mode.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • README.md
  • benches/reports/v0.2.5/bench_report_v0.2.5.md
✅ Files skipped from review due to trivial changes (1)
  • README.md

Comment thread benches/reports/v0.2.5/bench_report_v0.2.5.md Outdated
Comment thread benches/reports/v0.2.5/bench_report_v0.2.5.md Outdated
JoshuaChi and others added 3 commits June 2, 2026 23:30
- Standalone notes: scope "%%%%%%%%%%%%" to exclude Lease Read (which shows +10.8%)within
- AWS Embedded notes: remove incorrect ReadActor attribution for Linearizable Read;
  Lin Read path is unchanged by #392, +5.7% is an observed benchmark delta only

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces Arc<ArcSwap<Option<Arc<DB>>>> with ArcSwapOption<DB>.
The outer Arc was unused — no caller ever cloned the field independently.
Call sites become guard.as_deref() and store(None), no behavior change.
…eceive time

The safety invariant lease_duration_ms < election_timeout_min was enforced by
config validation, but the effective lease duration was silently extended by
~RTT/2 because update_lease_timestamp() used now_ms() at ACK *receive* time.

The follower's election timer resets at heartbeat *receive* time (~RTT/2 earlier),
so the old deadline = ack_ts + lease_duration_ms could equal or exceed the
earliest possible election, violating the invariant in practice (Jepsen set
workload confirmed: 19 elements lost under network partition).

Fix: record last_heartbeat_send_ts in Phase 0 of execute_and_process_raft_rpc
(before AppendEntries goes out). Multi-voter quorum ACK path now computes
deadline = send_ts + lease_duration_ms, matching etcd/TiKV practice.
Single-voter path unchanged (no peer RTT to account for).

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@d-engine-core/src/raft_role/leader_state_test/replication_test.rs`:
- Around line 1401-1407: The fixed 1ms sleep in the test is timing-fragile;
replace it with a bounded wait that polls crate::raft_role::read_lease::now_ms()
until it returns > 0 (with a short overall timeout) before calling process_batch
so the assertion on last_heartbeat_send_ts is deterministic; update the setup in
replication_test.rs (the block that currently calls
crate::raft_role::read_lease::init_clock() and std::thread::sleep) to loop-check
now_ms() > 0 and error/fail the test if the timeout elapses.

In `@d-engine-core/src/raft_role/leader_state.rs`:
- Around line 1899-1908: The lease-renewal uses the shared mutable
last_heartbeat_send_ts (set by execute_and_process_raft_rpc) causing stale ACKs
to renew using a newer global timestamp; instead propagate a per-request send_ts
through the AppendEntries request/response path and use that per-ACK timestamp
when calling update_lease_timestamp. Concretely: when building/sending
AppendEntries capture now_ms() into a local send_ts, attach/pass that send_ts
through the RPC call/response handling (e.g., via the
execute_and_process_raft_rpc callsite and its response callback) and replace
uses of last_heartbeat_send_ts in the ACK/quorum handling with the attached
send_ts so each ACK renews the lease with the timestamp tied to that specific
dispatched AppendEntries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b98e476c-1aac-49ea-8aac-e644bbac018d

📥 Commits

Reviewing files that changed from the base of the PR and between ccaecec and 0ed51c3.

⛔ Files ignored due to path filters (3)
  • benches/reports/v0.2.5/d-engine_comparison_v0.2.5.png is excluded by !**/*.png
  • benches/reports/v0.2.5/d-engine_v0.2.5_vs_v0.2.4_embedded_mode.png is excluded by !**/*.png
  • benches/reports/v0.2.5/d-engine_v0.2.5_vs_v0.2.4_standalone_mode.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • .gitignore
  • README.md
  • benches/reports/v0.2.5/bench_report_v0.2.5.md
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-core/src/raft_role/leader_state_test/lease_send_ts_test.rs
  • d-engine-core/src/raft_role/leader_state_test/mod.rs
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
✅ Files skipped from review due to trivial changes (3)
  • .gitignore
  • README.md
  • benches/reports/v0.2.5/bench_report_v0.2.5.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • d-engine-core/src/raft_role/leader_state_test/mod.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs

Comment thread d-engine-core/src/raft_role/leader_state_test/replication_test.rs
Comment thread d-engine-core/src/raft_role/leader_state.rs
…o lease_duration_ms + rtt_p99/2 < election_timeout_min

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@d-engine-core/src/config/raft.rs`:
- Around line 1080-1088: The docstring for the field/parameter
network_rtt_p99_ms is misleadingly labeled "one-way network RTT" while the code
treats it as round-trip (it is later divided by 2), which can cause operators to
under-estimate margins; update the comment near network_rtt_p99_ms in raft.rs to
state clearly that this value is the p99 round‑trip time (RTT) in milliseconds
and that the safety formula used is lease_duration_ms + (network_rtt_p99_ms / 2)
< election_timeout_min, referencing the symbols network_rtt_p99_ms,
lease_duration_ms and election_timeout_min so readers know the value is halved
when converting to one-way delay.
- Around line 1149-1156: ReadConsistencyConfig::validate() currently does
unchecked u64 addition on lease_duration_ms + rtt_half_ms which can overflow;
change the logic to use checked_add (or equivalent) to detect overflow and
return an Err(Config(ConfigError::Message(...))) when addition would overflow
instead of performing a wrap/unchecked add, then use the checked sum for the
comparison against election_timeout_min (include the same contextual message
mentioning lease_duration_ms and network_rtt_p99_ms/2 and that the sum must be
strictly less than election_timeout_min).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc85752a-0b08-4755-a40a-71a6db15bcf7

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed51c3 and f2db99a.

📒 Files selected for processing (3)
  • d-engine-core/src/config/raft.rs
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs

Comment thread d-engine-core/src/config/raft.rs Outdated
Comment thread d-engine-core/src/config/raft.rs
@JoshuaChi
JoshuaChi merged commit 64a3c9a into main Jun 7, 2026
10 of 11 checks passed
@JoshuaChi
JoshuaChi deleted the perf/392-read branch June 7, 2026 00:57
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.

perf: Lease/Eventual Read throughput regression in v0.2.4 (~8–10% on AWS 3-node)

1 participant