Skip to content

fix #315: isolate snapshot disk I/O from Raft event loop via spawn_blocking - #362

Merged
JoshuaChi merged 2 commits into
mainfrom
feature/315_snapshot_transfer
Apr 14, 2026
Merged

JoshuaChi merged 2 commits into
mainfrom
feature/315_snapshot_transfer

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented Apr 13, 2026 •

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Moves all blocking RocksDB snapshot I/O (create_dir_all, flush_cf_opt,
export_column_family) into tokio::task::spawn_blocking, preventing snapshot
generation from starving the Raft event loop's async worker threads and causing
intermittent 4006 ProposeFailed errors under concurrent write load.

Type:

  • Bug Fix (with test)

Why Is This Needed?

create_snapshot() in RocksDBStateMachine performed blocking disk I/O directly
on tokio async worker threads — the same pool that drives the Raft event loop.
Under concurrent write load (e.g. parallel test runs), these blocking operations
consumed async workers long enough to delay AppendEntries ack processing past the
proposal timeout, producing intermittent Error code: 4006 (ProposeFailed) in
test_learner_snapshot_concurrent_replication.

The fix wraps all blocking ops in spawn_blocking, which offloads them to tokio's
dedicated blocking thread pool, keeping async workers free for Raft protocol
processing. load_full() is used to obtain Arc<DB> (Send) for safe transfer
into the closure.

Also adds a minimum 2-core CPU recommendation to CHANGELOG: on single-core machines
all thread pools share one CPU and exhibit the same starvation regardless of
spawn_blocking.


Checklist

Required:

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

Testing

How tested:

  • Integration tests: test_learner_snapshot_concurrent_replication — previously
    failed intermittently with 4006 under make test (parallel run); now passes
    reliably because snapshot I/O no longer competes with the Raft event loop.

For bug fixes:

  • Added test that fails without this fix (test_learner_snapshot_concurrent_replication)

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 change is a mechanical wrapping of an existing synchronous block inside
spawn_blocking. The only subtle point is db.load_full() vs db.load() —
load_full() returns Arc<DB> which is Send and can be moved into the closure,
while load() returns a guard that is not.

Estimated review complexity:

  • Quick (< 100 lines)

Summary by CodeRabbit

  • Documentation

    • Added operational guidance recommending a minimum of 2 CPU cores per node and noting latency degradation on single-core machines under write load.
  • Refactor

    • Made snapshot export non-blocking to improve responsiveness and reliability; improved background export behavior and error handling during snapshot creation.

…ocking

RocksDB snapshot generation (create_dir_all, flush_cf_opt,
export_column_family) was executing on tokio async worker threads,
competing directly with the Raft event loop for thread pool capacity.
Under concurrent write load this caused AppendEntries acks to be
delayed past the proposal timeout, producing intermittent 4006
ProposeFailed errors in test_learner_snapshot_concurrent_replication.

Move all blocking snapshot I/O into tokio::task::spawn_blocking so
it runs on the dedicated blocking thread pool, freeing async workers
for Raft protocol processing. Use load_full() to obtain Arc<DB>
(Send) for safe transfer into the closure.

Also document the minimum 2-core CPU recommendation in CHANGELOG,
as single-core machines conflate all thread pools onto one CPU and
exhibit the same starvation pattern regardless of spawn_blocking.
@coderabbitai

coderabbitai Bot commented Apr 13, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@JoshuaChi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 23 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5290883-e6bb-4922-bb95-9f270add4c6b

📥 Commits

Reviewing files that changed from the base of the PR and between 55d9a71 and 59e74bf.

📒 Files selected for processing (2)
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine_test.rs
📝 Walkthrough

Walkthrough

Added operational notes to CHANGELOG recommending >=2 CPU cores per node; refactored generate_snapshot_data() to run RocksDB and filesystem work inside tokio::task::spawn_blocking, awaiting the join and mapping panics/cancellation into StorageError results.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md
Added "⚙️ Operational Notes" under v0.2.4 recommending a minimum of 2 CPU cores per node and explaining WAL IO thread plus Tokio worker threads and single-core latency degradation.
Snapshot Generation (RocksDB)
d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs
Refactored generate_snapshot_data() to clone new_snapshot_dir, move a loaded DB handle into a tokio::task::spawn_blocking closure that does create_dir_all, flushes CFs, Checkpoint::export_column_family for sm and sm_meta, and save_cf_export_metadata; async method now awaits the join and maps join/panic errors into StorageError.

Sequence Diagram

sequenceDiagram
    participant Async as Async Context
    participant Tokio as Tokio Runtime
    participant Block as Blocking Thread Pool
    participant RocksDB as RocksDB + Filesystem
    participant Err as Error Mapper

    Async->>Tokio: call generate_snapshot_data()
    Tokio->>Tokio: prepare cloned dir & DB handle
    Tokio->>Block: spawn_blocking(snapshot task)
    Block->>RocksDB: create directories
    Block->>RocksDB: flush column families
    Block->>RocksDB: export checkpoints (sm, sm_meta)
    Block->>RocksDB: write export metadata
    Block-->>Tokio: return Result<(), io/db error>
    Tokio->>Err: await join, map panic/cancel -> StorageError
    Err-->>Async: return Result<(), StorageError>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped to the blocking pool with glee,
RocksDB hums while I sip my tea,
Threads align — no async spree,
Snapshots saved, safe as can be,
thump thump 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main change: isolating snapshot disk I/O from the Raft event loop using spawn_blocking, which directly addresses issue #315.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/315_snapshot_transfer

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 the current code and only fix it if needed.

Inline comments:
In `@d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs`:
- Around line 911-912: The current map_err treats all JoinError as "panicked";
update the closure that handles the .await result for the snapshot blocking task
to distinguish cancellation vs panic by inspecting the JoinError (use
e.is_cancelled() and e.is_panic() on the JoinError), returning a
StorageError::DbError with a clear "snapshot blocking task cancelled" message
for cancelled tasks and preserving the "panicked" message (including error
details) for actual panics; replace the single map_err(|e|
StorageError::DbError(format!("snapshot blocking task panicked: {e}")))?? with a
match/if that checks e.is_cancelled() first, then e.is_panic(), and otherwise
includes the JoinError info.
🪄 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: c3a598d2-5521-4867-aef3-6ef7e5e9b995

📥 Commits

Reviewing files that changed from the base of the PR and between 87fc748 and 7f2438d.

⛔ Files ignored due to path filters (3)
  • 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 (2)
  • CHANGELOG.md
  • d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs

Comment thread d-engine-server/src/storage/adaptors/rocksdb/rocksdb_state_machine.rs Outdated
@codecov

codecov Bot commented Apr 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...age/adaptors/rocksdb/rocksdb_state_machine_test.rs 86.95% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@JoshuaChi
JoshuaChi force-pushed the feature/315_snapshot_transfer branch from 55d9a71 to 59e74bf Compare April 14, 2026 03:20
@JoshuaChi
JoshuaChi merged commit 5f5be93 into main Apr 14, 2026
9 checks passed
@JoshuaChi
JoshuaChi deleted the feature/315_snapshot_transfer branch April 14, 2026 03:46
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