Skip to content

perf(read) #390: linearizable read lease fast path + fix Raft lease clock (SystemTime → Instant) - #391

Merged
JoshuaChi merged 3 commits into
mainfrom
perf/390-linear-lease
May 24, 2026
Merged

JoshuaChi merged 3 commits into
mainfrom
perf/390-linear-lease

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Adds a lease fast path for LinearizableRead: when the leader holds a valid lease
and last_applied >= read_index, reads are served without a consensus round-trip.
Also fixes a correctness bug where the lease clock used SystemTime (susceptible to
NTP step-backs) instead of Instant (always monotonic).

Type:

  • Bug Fix (with test)
  • Feature (issue #___ approved)
  • Documentation
  • Test/Coverage
  • Performance (with benchmark)

Why Is This Needed?

Issue: #390

LinearizableRead in multi-voter clusters always required a full consensus round-trip
(Raft §8 step 2). Under high concurrency this becomes the throughput bottleneck.

Raft §6.4 allows the leader to skip the round-trip when it holds a valid lease —
"valid" meaning a quorum ACK arrived within lease_duration_ms, which proves no
higher-term leader can exist at that instant. The existing LeaseRead path already
had the concept; this PR extends it to LinearizableRead.

Two bugs fixed in the process:

  1. lease_timestamp: AtomicU64 stored a raw SystemTime epoch. NTP stepping the
    clock backward extends the lease window silently → potential stale read. Fixed to
    Mutex<Option<Instant>>.
  2. Config validation was a warn! for lease_duration_ms >= election_timeout_min.
    This bound is load-bearing for safety; changed to a hard ConfigError.

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

How tested:

  • Unit tests: pending_reads_test.rs — 199 lines covering: lease-valid fast path,
    lease-expired slow path, single-voter path, last_applied < read_index still queues,
    clock monotonicity (Instant vs SystemTime)
  • Unit tests: raft_test.rs — 54 lines covering config validation (lease ≥ election
    timeout must error)
  • Manual testing: AWS EC2 3-node c5.2xlarge, 5-round bench (see benchmark below)

For performance improvements:

  • Included benchmark showing improvement

Benchmark (AWS EC2 3-node c5.2xlarge, 8 vCPU/16GB, key=8B val=256B):

Scenario Before (#381) After (#390) Δ
Standalone Lin Read throughput ~51K ops/s ~77K ops/s +52%
Standalone Lin Read avg latency ~3.9 ms ~2.6 ms -34%
Embedded Lin Read throughput ~321K ops/s ~327K ops/s +2%

Full report: benches/reports/v0.2.4/bench_report_v0.2.4.md


Does This Follow d-engine's Principles?

  • Solves a real problem for most users (not just my edge case)
  • Keeps implementation simple
  • Doesn't bloat the API surface

Reviewer Notes

The safety argument for the fast path rests on two invariants enforced at config load:

  1. lease_duration_ms < election_timeout_min (now a hard error, not a warning)
  2. Lease clock is Instant — NTP cannot rewind it

The fast path touches only handle_linearizable_read_requests in leader_state.rs.
is_lease_valid() is the single point of truth; it returns false when
lease_instant is None (no quorum ACK received yet), ensuring cold-start safety.

Estimated review complexity:

  • Quick (< 100 lines)
  • Medium (< 300 lines)
  • Deep (> 300 lines)

Summary by CodeRabbit

  • New Features

    • Linearizable read fast path via valid leader lease for improved read performance
    • Watch enhancement with prev_kv and periodic progress events
    • New scan_prefix client API for efficient prefix scans
  • Bug Fixes

    • Multi-voter linearizable reads now require quorum after partitions
  • Changed

    • apply_chunk signature change for custom state machines (see migration guide)
    • Config validation: lease duration must be less than election timeout
  • Documentation

    • Updated README, migration guide, benchmarking report, and FAQs
  • Chores

    • Added clean-log-db make target

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR releases d-engine v0.2.4: adds a lease-based linearizable-read fast path for multi-voter leaders, refactors leader lease tracking to use monotonic Instants, enforces strict lease/election timing validation, updates read dispatch logic, adds regression tests, and updates docs, changelog, versions, and benchmarks.

Changes

d-engine v0.2.4 Release: Linearizable Read Lease Fast Path

Layer / File(s) Summary
Release versioning, changelog, migration guide, README, and benchmarks
Cargo.toml, CHANGELOG.md, MIGRATION_GUIDE.md, README.md, benches/reports/v0.2.4/*, d-engine/src/docs/performance/benchmarking-guide.md
Workspace bumped to v0.2.4; changelog documents lease fast-path, watch prev_kv/progress, scan_prefix API, multi-voter read fix, and StateMachine::apply_chunk signature change; migration guide shows old vs new apply_chunk; README and benchmark report updated (May 2026).
Lease timing config validation
d-engine-core/src/config/raft.rs, d-engine-core/src/config/raft_test.rs
RaftConfig::validate now errors if read_consistency.lease_duration_ms >= election.election_timeout_min; tests added for equal/greater/less cases.
Lease timestamp storage refactoring
d-engine-core/src/raft_role/leader_state.rs
Replace lease_timestamp: AtomicU64 with lease_instant: Mutex<Option<Instant>>; update imports and initialize to None.
Lease validity checking methods
d-engine-core/src/raft_role/leader_state.rs
Reimplement is_lease_valid() and update_lease_timestamp() using monotonic Instant protected by a mutex; lease valid only when instant exists and elapsed < lease_duration_ms.
Multi-voter linearizable read fast path
d-engine-core/src/raft_role/leader_state.rs
Phase-3 read dispatch now allows multi-voter leaders to serve linearizable reads immediately when is_lease_valid() is true and last_applied >= read_index; otherwise reads are queued for quorum/SM readiness.
Lease fast-path regression tests
d-engine-core/src/raft_role/leader_state_test/pending_reads_test.rs
Three tests added verifying: (1) valid lease + SM current → immediate serve; (2) valid lease + SM behind → queued; (3) expired/absent lease + SM current → queued; addresses Bug #390.
Docs, examples, and tooling updates
d-engine/src/docs/use-cases.md, benches/embedded-bench/Makefile, examples/.../Makefile
Add “Migrating from etcd?” guidance, add benchmark cleanup make target, and adjust example Makefile messaging.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • deventlab/d-engine#229: Related changes to linearizable-read path in leader_state.rs; similar safety adjustments.
  • deventlab/d-engine#230: Related work ensuring state-machine progress before serving linearizable reads.
  • deventlab/d-engine#383: Prior modifications to Phase-3 read serving logic; closely related to this PR's quorum/lease gating.

Poem

🐰 Upon the leader's steady tick, I hop and hum a tune,
Monotonic time keeps leases safe beneath the moon,
Multi-voters may reply when lease and SM agree,
No stale reads sneak past the gate, the cluster stays debris-free,
Hooray for fast-path hops and tests that make it true!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: a lease fast path for linearizable reads and a clock fix from SystemTime to Instant, matching the core implementation work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/390-linear-lease

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

❤️ Share

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

@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 `@CHANGELOG.md`:
- Line 90: The changelog line describing StateMachine::apply_chunk should use
American English "implementers" instead of "implementors"; update the sentence
that reads "Custom state machine implementors must update their `impl`." to
"Custom state machine implementers must update their `impl`." while keeping
references to `StateMachine::apply_chunk` and `ApplyEntry` intact.
🪄 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: cfd0e842-7ac6-42e0-bc37-e5d8e7b1530e

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0c697 and 05c5c89.

⛔ Files ignored due to path filters (7)
  • Cargo.lock is excluded by !**/*.lock
  • benches/reports/v0.2.4/d-engine_comparison_v0.2.4.png is excluded by !**/*.png
  • benches/reports/v0.2.4/d-engine_v0.2.3_vs_v0.2.4_embedded_mode.png is excluded by !**/*.png
  • benches/reports/v0.2.4/d-engine_v0.2.3_vs_v0.2.4_standalone_mode.png is excluded by !**/*.png
  • examples/client-usage-standalone/Cargo.lock is excluded by !**/*.lock
  • examples/single-node-expansion/Cargo.lock is excluded by !**/*.lock
  • examples/three-nodes-embedded/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • CHANGELOG.md
  • Cargo.toml
  • MIGRATION_GUIDE.md
  • README.md
  • benches/reports/v0.2.4/bench_report_v0.2.4.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/pending_reads_test.rs
  • d-engine/src/docs/performance/benchmarking-guide.md

Comment thread CHANGELOG.md
@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.01709% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../raft_role/leader_state_test/pending_reads_test.rs 94.66% 4 Missing ⚠️
d-engine-core/src/config/raft_test.rs 88.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

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

Caution

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

⚠️ Outside diff range comments (2)
benches/embedded-bench/Makefile (1)

4-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add clean-log-db to .PHONY to avoid accidental no-op runs.

clean-log-db should be declared phony; otherwise make may skip it if a file/dir with that name exists. Update Line 4 to include clean-log-db.

Also applies to: 73-77

🤖 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 `@benches/embedded-bench/Makefile` at line 4, Add the missing phony declaration
for the Makefile target by including "clean-log-db" in the .PHONY list (the
.PHONY line that currently lists help build clean test-single-write ...
all-tests) so make won't skip the target if a file/dir named clean-log-db
exists; also add "clean-log-db" to the other .PHONY declaration(s) around the
later block that lists targets (the block that includes test-high-conc-write
test-linearizable-read test-lease-read test-eventual-read test-hot-key) to
ensure all occurrences declare the target as phony.
benches/reports/v0.2.4/bench_report_v0.2.4.md (1)

99-99: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix likely broken image filename in markdown link.

Line 99 includes a space in the asset name (...v0.2.4_ vs...), which is likely an invalid path and will break image rendering.

🤖 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 `@benches/reports/v0.2.4/bench_report_v0.2.4.md` at line 99, The markdown image
link contains an unintended space in the asset name ("d-engine_v0.2.4_
vs_etcd_3.2.0.png") which will break rendering; fix the filename in the image
reference used in the README line (the `![d-engine v0.2.4 vs etcd
3.2.0](d-engine_v0.2.4_ vs_etcd_3.2.0.png)` entry) by removing or replacing the
space (e.g., `d-engine_v0.2.4_vs_etcd_3.2.0.png`) or by URL-encoding/quoting the
path so it matches the actual asset filename.
🤖 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.

Outside diff comments:
In `@benches/embedded-bench/Makefile`:
- Line 4: Add the missing phony declaration for the Makefile target by including
"clean-log-db" in the .PHONY list (the .PHONY line that currently lists help
build clean test-single-write ... all-tests) so make won't skip the target if a
file/dir named clean-log-db exists; also add "clean-log-db" to the other .PHONY
declaration(s) around the later block that lists targets (the block that
includes test-high-conc-write test-linearizable-read test-lease-read
test-eventual-read test-hot-key) to ensure all occurrences declare the target as
phony.

In `@benches/reports/v0.2.4/bench_report_v0.2.4.md`:
- Line 99: The markdown image link contains an unintended space in the asset
name ("d-engine_v0.2.4_ vs_etcd_3.2.0.png") which will break rendering; fix the
filename in the image reference used in the README line (the `![d-engine v0.2.4
vs etcd 3.2.0](d-engine_v0.2.4_ vs_etcd_3.2.0.png)` entry) by removing or
replacing the space (e.g., `d-engine_v0.2.4_vs_etcd_3.2.0.png`) or by
URL-encoding/quoting the path so it matches the actual asset filename.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8dcaf04-b292-4e79-97c7-05d609c655bc

📥 Commits

Reviewing files that changed from the base of the PR and between 05c5c89 and fc82efe.

📒 Files selected for processing (5)
  • README.md
  • benches/embedded-bench/Makefile
  • benches/reports/v0.2.4/bench_report_v0.2.4.md
  • d-engine/src/docs/use-cases.md
  • examples/three-nodes-standalone/Makefile
✅ Files skipped from review due to trivial changes (2)
  • examples/three-nodes-standalone/Makefile
  • README.md

@JoshuaChi
JoshuaChi merged commit 3797413 into main May 24, 2026
9 checks passed
@JoshuaChi
JoshuaChi deleted the perf/390-linear-lease branch May 24, 2026 04:03
@JoshuaChi
JoshuaChi restored the perf/390-linear-lease branch May 24, 2026 04:03
@JoshuaChi
JoshuaChi deleted the perf/390-linear-lease branch May 24, 2026 04:07
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.

1 participant