Skip to content

perf #407: coalesce AppendEntries and decouple Raft event processing layers - #411

Merged
JoshuaChi merged 6 commits into
mainfrom
perf/403-role-event-starve
Jun 20, 2026
Merged

JoshuaChi merged 6 commits into
mainfrom
perf/403-role-event-starve

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Introduces a VecDeque buffer between the select! loop and Raft event processing, then adds merge_append_entries() to coalesce consecutive same-term AppendEntries events — including heartbeat absorption via max(leader_commit_index) — before dispatch. Also corrects durability comments and docs that misrepresented MemFirst as "not power-loss safe."

Type:

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

Why Is This Needed?

For features: Issue #407

Under high replication load, the select! loop received one AppendEntries event per iteration. Each event triggered a full follower append cycle (log write + durable_index advance + response), serialising what could be a single batched write. Role events (heartbeats, votes) arriving between AEs also prevented natural batching. Decoupling select! from processing via buffered VecDeques allows merge_append_entries() to coalesce the leading run of contiguous AEs — including heartbeat absorption — into one dispatch, reducing fsync round-trips and follower response round-trips per unit of replicated data.


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: merge_append_entries_tests.rs — 14 tests covering: empty buffer, single event, basic 3-way merge, metadata preservation, non-contiguous stop, non-AE FIFO stop, heartbeat absorption (mid/end/consecutive/all-HB), sender collection order, dropped receiver, term ordering (newer-first stop), max_merge_entries cap
  • Integration tests: existing make test suite
  • Manual testing: n/a

For performance improvements:


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

AppendEntries second field changes from MaybeCloneOneshotSender → Vec<MaybeCloneOneshotSender> — all call sites updated. The merge_append_entries() method is pub(crate) only, no public API change.

Durability comment fix: is_write_durable() = false + flush() = flush_wal(true)/sync_all() → committed data is disk-durable (batched Level 3). throughput-optimization-guide.md and inline comments corrected accordingly.

drain_based_batch_architecture_tests.rs has pre-existing compile errors (orphaned before this branch, now surfaced via #[path]) — tracked separately, not part of this PR.

Estimated review complexity:

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

Summary by CodeRabbit

  • New Features

    • Added max_merge_entries to cap consecutive AppendEntries coalescing.
    • Improved Raft event processing by coalescing buffered AppendEntries (including heartbeats).
    • Added speculative next_index advancement before append-result feedback.
  • Bug Fixes

    • Prevent leader next_index regressions on stale conflict acknowledgements with refined clamp/update logic.
    • Improved robustness dispatching AppendEntries responses to multiple recipients by logging per-recipient send failures without aborting processing.
  • Documentation

    • Refined MemFirst/flush and batched “Level 3” durability guidance.

@coderabbitai

coderabbitai Bot commented Jun 19, 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: 15369032-ba33-48b0-9ec3-b4cc25d0c85a

📥 Commits

Reviewing files that changed from the base of the PR and between 2742334 and 2d5e010.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • d-engine-core/src/raft_test/process_raft_events_tests.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • d-engine-core/src/raft_test/process_raft_events_tests.rs

📝 Walkthrough

Walkthrough

The PR introduces a drain-based buffering architecture in the Raft main loop, adding VecDeque queues for raft and role events. A new merge_append_entries function coalesces consecutive AppendEntries events sharing the same term and prev_log_index. To support merging, RaftEvent::AppendEntries now carries a Vec of response senders instead of a single sender. Leader replication index tracking is refactored with speculative next_index advancement based on outgoing request sizes and a new update_peer_index helper that prevents regression on success and clamps conflict retreats. RocksDB::is_write_durable is corrected to return false, tying durable index advancement to WAL fsync completion. Comprehensive test suites validate merge behavior, drain-loop dispatch, and role-state transitions.

Changes

AppendEntries Batching and Merge Pipeline

Layer / File(s) Summary
RaftEvent contract and BatchingConfig
d-engine-core/src/event.rs, d-engine-core/src/config/raft.rs
RaftEvent::AppendEntries second field changes from a single sender to Vec<MaybeCloneOneshotSender>. unsafe impl Sync is added for RaftEvent. BatchingConfig gains max_merge_entries (default 1000) with a zero-check in validate().
Raft main loop buffering and merge_append_entries
d-engine-core/src/raft.rs, d-engine-core/src/raft_test/mod.rs
Raft<T> gains buffered_raft_event and buffered_role_event VecDeque fields. tokio::select! arms enqueue events and drain additional pending items (bounded by max_batch_size). Post-select, process_role_events, process_client_cmds, and process_raft_events run in order. process_raft_events first calls merge_append_entries, which coalesces contiguous same-term AppendEntries up to max_merge_entries, accumulating entries and sender vectors. Test module declarations move from mod.rs to raft.rs via #[path].
Role-state multi-sender dispatch
d-engine-core/src/raft_role/role_state.rs, d-engine-core/src/raft_role/candidate_state.rs, d-engine-core/src/raft_role/leader_state.rs
handle_append_entries_request_workflow accepts Vec<MaybeCloneOneshotSender> and iterates over all senders in all response paths (higher-term, success, conflict), logging per-recipient failures. Candidate and leader AppendEntries arms destructure and forward or broadcast to the full sender collection.
Leader speculative next_index and update_peer_index
d-engine-core/src/raft_role/leader_state.rs
update_peer_indexes (bulk HashMap) is replaced by pub(super) update_peer_index(follower_id, &PeerUpdate). Success advances next_index via max(current, hint) to prevent regression; conflict retreats to the hint with a match_index+1 floor. Before dispatching each AppendEntries to a worker, the leader speculatively advances next_index to prev_log_index + entries.len() + 1.
gRPC wiring and RocksDB durability fix
d-engine-server/src/network/grpc/grpc_raft_service.rs, d-engine-server/src/storage/adaptors/rocksdb/rocksdb_storage_engine.rs, d-engine-core/src/storage/buffered_raft_log.rs
RaftEvent::AppendEntries dispatch in both unary and streaming RPC handlers wraps resp_tx in vec![resp_tx]. is_write_durable returns false (was true), tying durable index advancement to flush_wal(true) completion. advance_durable_after_write documentation is updated to describe batched Level 3 durability semantics.
merge_append_entries comprehensive test suite
d-engine-core/src/raft_test/merge_append_entries_tests.rs
887 lines of new tests cover empty-buffer no-op, single-event preservation, first-event-non-AppendEntries no-op, basic multi-event merge (entry accumulation, max commit index, metadata from first request), boundary stops on term mismatch and non-contiguous prev_log_index, heartbeat absorption and consolidation, non-AppendEntries event interrupting merge while preserving FIFO order, sender FIFO collection order, dropped-receiver safety, term-ordering boundaries, and max_merge_entries configuration limits.
process_raft_events drain-loop and merge-guard tests
d-engine-core/src/raft_test/process_raft_events_tests.rs
285 lines validating Raft's drain-loop merge and dispatch behavior. Tests verify merge dispatch counts when AppendEntries are separated by non-AppendEntries events, ensure all-non-AppendEntries buffers drain without merge logic or panicking, validate fatal role events propagate correctly via err.is_fatal(), and confirm isolated AppendEntries are dispatched without coalescing.
Role-state and leader test updates
d-engine-core/src/raft_role/candidate_state_test.rs, d-engine-core/src/raft_role/follower_state_test.rs, d-engine-core/src/raft_role/learner_state_test.rs, d-engine-core/src/raft_role/leader_state_test/...
All RaftEvent::AppendEntries test constructions are updated to vec![resp_tx]. New tests include test_speculative_next_index_before_receiving_append_result validating speculative advancement, stale-conflict floor regression tests ensuring next_index does not regress below match_index+1, and mod test_leader_update_next_index with four unit tests covering update_peer_index semantics (success preserves speculation, conflict retreats/clamps). Import blocks are reorganized in commit_index_test.rs, replication_test.rs, and client_write_test.rs.
Documentation and minor cleanup
d-engine/src/docs/performance/throughput-optimization-guide.md, benches/standalone-bench/src/main.rs, d-engine-core/src/raft_test/drain_based_batch_architecture_tests.rs, CHANGELOG.md
Throughput guide rewrites the MemFirst description to cover fsync-before-durable-index, process-crash vs power-loss scenarios, idle_flush_interval_ms bounds, and the DiskFirst removal note. Benchmark file reorders imports and removes dereferencing from a println! argument. drain_based_batch_architecture_tests imports are split and reorganized for clarity. Changelog documents the new AppendEntries coalescing and speculative replication feature.

Sequence Diagram(s)

sequenceDiagram
  participant gRPC as gRPC AppendEntries Handler
  participant Channel as raft_event_tx
  participant Raft as Raft Main Loop
  participant Drain as Drain Helpers
  participant Merge as merge_append_entries
  participant Role as RaftRoleState
  participant gRPCResp as Response Handlers

  gRPC->>Channel: RaftEvent::AppendEntries(req, vec![resp_tx])
  Channel->>Raft: tokio::select! fires raft arm
  Raft->>Raft: push_back to buffered_raft_event
  Raft->>Drain: drain_raft_events (up to max_batch_size)
  Drain->>Raft: try_recv additional events into buffer
  Raft->>Merge: process_raft_events: merge_append_entries(...)
  Merge->>Merge: coalesce contiguous same-term AppendEntries
  Merge->>Merge: accumulate entries and senders Vec
  Merge->>Raft: rewrite buffered_raft_event with merged events
  Raft->>Role: handle_raft_event(merged AppendEntries, Vec<senders>)
  Role->>Role: handle_append_entries_request_workflow(..., senders)
  Role->>gRPCResp: iterate senders: send response
  gRPCResp->>gRPC: oneshot::Sender::send result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • fix(test): rewrite drain_based_batch_architecture_tests against core types #410: The import reorganization in drain_based_batch_architecture_tests.rs (adding ClientReadRequest, ClientWriteRequest, ReadConsistencyPolicy from crate::client and splitting proto imports) directly addresses the type mismatch described in that issue by rewriting the orphaned test file to use core types rather than proto-generated types.

Poem

🐇 Hop hop, the buffers grow,
AppendEntries lined up in a row,
Merge them fast, one by one,
vec![resp_tx] — it's done!
Speculative leaps before the ACK,
This bunny never looks back. 🚀

🚥 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 title directly and specifically captures the main changes: coalescing AppendEntries events and decoupling Raft event processing layers, which aligns with the core technical improvements in the PR.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/403-role-event-starve

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

🤖 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_test/merge_append_entries_tests.rs`:
- Around line 855-856: The test at line 855-856 with the loop containing the
tuple [(9u64, 5, 6), (14, 5, 5)] is validating term-boundary behavior rather
than max_merge_entries cap enforcement because the two test cases use different
terms (6 and 5), causing merge_append_entries() to stop at the term mismatch
before evaluating the max_merge_entries guard. Fix this by modifying both test
cases to use the same term value so that the max_merge_entries cap enforcement
is actually tested rather than the term boundary logic. Ensure the prev and
count values remain as parameters to properly exercise the cap behavior while
keeping the term consistent across both iterations.

In `@d-engine-core/src/raft.rs`:
- Around line 388-393: The error handling in the `process_role_events` method
currently swallows all errors returned by `handle_role_event`, including fatal
errors like `RoleEvent::FatalError`, which prevents the node from properly
exiting when a fatal condition occurs. Refactor the error handling in
`process_role_events` to distinguish between fatal and non-fatal errors,
matching the fatal/non-fatal error handling pattern already implemented in the
`process_raft_events` method. Fatal errors should be propagated by returning
them from `process_role_events`, while non-fatal errors should continue to be
logged and ignored.
- Around line 397-416: The merge_append_entries() function is only called once
before the while loop begins in the process_raft_events method, which means if
the buffered queue contains non-AppendEntries events (like VoteRequest)
interspersed with AppendEntries events, the later consecutive AppendEntries runs
will not be merged. Move the merge_append_entries() call inside the while loop
so it executes before each event is processed, ensuring that all consecutive
runs of AppendEntries entries are merged regardless of their position in the
queue, not just the prefix entries.
- Around line 446-448: The merge guard condition for RaftEvent::AppendEntries is
incomplete and does not validate the log-matching property of Raft. The current
condition checks next_prev == req.prev_log_index and term == req.term, but it is
missing a check for prev_log_term consistency. Add an additional check to the
merge guard condition to verify that the log term at prev_log_index matches
req.prev_log_term, ensuring that the log-matching checks are preserved across
merged boundaries and preventing inconsistent append entries requests from being
accepted as part of a merged operation.
🪄 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: 32f85a0b-75ec-4ec0-8c65-59bf2919f168

📥 Commits

Reviewing files that changed from the base of the PR and between 64a3c9a and e5ec77a.

📒 Files selected for processing (22)
  • benches/standalone-bench/src/main.rs
  • d-engine-core/src/config/raft.rs
  • d-engine-core/src/event.rs
  • d-engine-core/src/raft.rs
  • d-engine-core/src/raft_role/candidate_state.rs
  • d-engine-core/src/raft_role/candidate_state_test.rs
  • d-engine-core/src/raft_role/follower_state_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/leader_state_test/client_write_test.rs
  • d-engine-core/src/raft_role/leader_state_test/commit_index_test.rs
  • d-engine-core/src/raft_role/leader_state_test/event_handling_test.rs
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs
  • d-engine-core/src/raft_role/learner_state_test.rs
  • d-engine-core/src/raft_role/role_state.rs
  • d-engine-core/src/raft_test/drain_based_batch_architecture_tests.rs
  • d-engine-core/src/raft_test/merge_append_entries_tests.rs
  • d-engine-core/src/raft_test/mod.rs
  • d-engine-core/src/storage/buffered_raft_log.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_storage_engine.rs
  • d-engine/src/docs/performance/throughput-optimization-guide.md
💤 Files with no reviewable changes (1)
  • d-engine-core/src/raft_test/mod.rs

Comment thread d-engine-core/src/raft_test/merge_append_entries_tests.rs Outdated
Comment thread d-engine-core/src/raft.rs
Comment thread d-engine-core/src/raft.rs
Comment thread d-engine-core/src/raft.rs
@codecov

codecov Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.86563% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rc/raft_role/leader_state_test/replication_test.rs 96.70% 6 Missing ⚠️
d-engine-core/src/raft_role/leader_state.rs 80.95% 4 Missing ⚠️
d-engine-core/src/config/raft.rs 66.66% 3 Missing ⚠️
d-engine-core/src/raft.rs 96.51% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Leader now advances next_index immediately when firing AppendEntries
(prev_log_index + entries.len() + 1) instead of waiting for ACK,
eliminating the stop-and-wait LEGACY resend on every round.

update_peer_index replaces update_peer_indexes with two distinct paths:
- Success: max(current, peer_match+1) — never regress speculative value
- Conflict: retreat to follower hint, floor-guarded by match_index+1

Add 7 unit tests covering speculative advance, success preservation,
conflict rollback, and floor guard correctness.
…layers

- Buffer raft/role events in VecDeque; merge contiguous same-term AEs,
  absorb heartbeats via max(leader_commit_index), cap via max_merge_entries
- AppendEntries sender → Vec to broadcast across merged events
- Add merge_append_entries_tests.rs
- Fix is_write_durable() comments: flush_wal(true) = batched Level 3, not unrecoverable
- Update throughput-optimization-guide.md: correct MemFirst durability semantics
…n loop

merge_append_entries was called once before the loop; AE runs following
a non-AE event were never coalesced. matches!() guard fires only when
front is AppendEntries, skipping push/pop cost for non-AE events.

Adds process_raft_events_tests.rs: drain-loop tests asserting via mock
times() — times(1) vs times(2) on handle_append_entries is the invariant.
@JoshuaChi
JoshuaChi force-pushed the perf/403-role-event-starve branch from e5ec77a to 2742334 Compare June 19, 2026 15:09

@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_test/process_raft_events_tests.rs`:
- Around line 237-239: The comment on the expect_handle_append_entries() call
incorrectly references "merged dispatch" which doesn't occur in this test—this
appears to be copy-pasted from another test. Replace the misleading comment with
an accurate description explaining that the 2 calls correspond to 2 append
entries that are each dispatched independently without any merging, reflecting
the actual behavior of VR isolation in this specific test.
- Around line 247-252: The inline comments describing entry ranges in the test
do not match the actual AppendEntries requests being created. Update the comment
above the first queue.push_back call with make_ae_event to correctly reflect
that it creates entries 10-14 (not 10-19) based on the make_request call with
prev=9 and 5 entries. Similarly, update the comment above the second
queue.push_back call with make_ae_event to correctly reflect that it creates
entries 15-19 (not 20-29) based on the make_request call with prev=14 and 5
entries. These comments were copy-pasted from an earlier test and need
correction to accurately describe the actual entry ranges in this test.
🪄 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: 1223c5ac-c04c-47e0-a121-121e0b9ab14e

📥 Commits

Reviewing files that changed from the base of the PR and between e5ec77a and 2742334.

📒 Files selected for processing (23)
  • benches/standalone-bench/src/main.rs
  • d-engine-core/src/config/raft.rs
  • d-engine-core/src/event.rs
  • d-engine-core/src/raft.rs
  • d-engine-core/src/raft_role/candidate_state.rs
  • d-engine-core/src/raft_role/candidate_state_test.rs
  • d-engine-core/src/raft_role/follower_state_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/leader_state_test/client_write_test.rs
  • d-engine-core/src/raft_role/leader_state_test/commit_index_test.rs
  • d-engine-core/src/raft_role/leader_state_test/event_handling_test.rs
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs
  • d-engine-core/src/raft_role/learner_state_test.rs
  • d-engine-core/src/raft_role/role_state.rs
  • d-engine-core/src/raft_test/drain_based_batch_architecture_tests.rs
  • d-engine-core/src/raft_test/merge_append_entries_tests.rs
  • d-engine-core/src/raft_test/mod.rs
  • d-engine-core/src/raft_test/process_raft_events_tests.rs
  • d-engine-core/src/storage/buffered_raft_log.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_storage_engine.rs
  • d-engine/src/docs/performance/throughput-optimization-guide.md
💤 Files with no reviewable changes (1)
  • d-engine-core/src/raft_test/mod.rs
✅ Files skipped from review due to trivial changes (4)
  • benches/standalone-bench/src/main.rs
  • d-engine-core/src/storage/buffered_raft_log.rs
  • d-engine-core/src/raft_test/drain_based_batch_architecture_tests.rs
  • d-engine-core/src/raft_role/leader_state_test/commit_index_test.rs
🚧 Files skipped from review as they are similar to previous changes (16)
  • d-engine-core/src/raft_role/leader_state_test/event_handling_test.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-core/src/config/raft.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_storage_engine.rs
  • d-engine-core/src/raft_role/follower_state_test.rs
  • d-engine-core/src/raft_role/leader_state_test/client_write_test.rs
  • d-engine-core/src/raft_role/role_state.rs
  • d-engine-core/src/raft_role/candidate_state.rs
  • d-engine-core/src/raft_role/candidate_state_test.rs
  • d-engine-core/src/raft_role/learner_state_test.rs
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-core/src/event.rs
  • d-engine/src/docs/performance/throughput-optimization-guide.md
  • d-engine-core/src/raft.rs
  • d-engine-core/src/raft_test/merge_append_entries_tests.rs

Comment thread d-engine-core/src/raft_test/process_raft_events_tests.rs Outdated
Comment thread d-engine-core/src/raft_test/process_raft_events_tests.rs
@JoshuaChi
JoshuaChi merged commit 09bd35c into main Jun 20, 2026
9 checks passed
@JoshuaChi
JoshuaChi deleted the perf/403-role-event-starve branch June 20, 2026 01:09
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