Skip to content

refactor(durability): rely on quorum replication, not per-node fsync#16

Merged
msallin merged 1 commit into
mainfrom
claude/cool-nobel-5074ba
Jun 24, 2026
Merged

refactor(durability): rely on quorum replication, not per-node fsync#16
msallin merged 1 commit into
mainfrom
claude/cool-nobel-5074ba

Conversation

@msallin

@msallin msallin commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Aligns the storage durability model with the dotNext maintainer's guidance in dotnet/dotNext#285: durability is a cluster property (a write is durable once committed to a quorum), not a per-node fsync.

  • Remove the per-write synchronous flush (DurablyFlushAsync and its gate). SubmitAsync returns once the entry is committed and applied. The per-write flush only hardened the handling node's disk, not the quorum, at the cost of throughput and CI fsync contention.
  • FlushInterval becomes a public tuning knob (default 100 ms) that batches the background flush; it trades fsync frequency against restart catch-up, not durability.
  • Flush the committed tail on graceful shutdown so a cleanly stopped node restarts already caught up.
  • Document single node as development-only (no fault tolerance) across the README, samples, and options; production needs an odd number of nodes.

Tests

Repurposed the single-node crash-durability test into a graceful-shutdown flush test. Multi-node durability remains covered by the three-node and quorum-loss tests. Local build/test was not possible in the dev environment (SDK 10.0.300 installed vs 10.0.301 pinned), so this relies on CI.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 51d5fe88-d1a3-48f7-a7c9-ca66c2298799

📥 Commits

Reviewing files that changed from the base of the PR and between c21afab and 5c2a8cc.

📒 Files selected for processing (6)
  • README.md
  • samples/Hangfire.Raft.K8sSample/Program.cs
  • samples/Hangfire.Raft.Sample/Program.cs
  • src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
  • src/Hangfire.Raft/RaftStorageOptions.cs
  • tests/Hangfire.Raft.Tests/RaftJobStorageTests.cs
✅ Files skipped from review due to trivial changes (3)
  • samples/Hangfire.Raft.K8sSample/Program.cs
  • README.md
  • samples/Hangfire.Raft.Sample/Program.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/Hangfire.Raft.Tests/RaftJobStorageTests.cs
  • src/Hangfire.Raft/RaftStorageOptions.cs
  • src/Hangfire.Raft/Cluster/RaftStorageCluster.cs

📝 Walkthrough

Walkthrough

The PR updates WAL durability wording and options documentation, removes synchronous flush handling from SubmitAsync, and simplifies disposal behavior. It also clarifies sample comments and removes a crash-survival test tied to disabled background flushing.

Changes

Background WAL flushing and durability wording

Layer / File(s) Summary
Option and documentation wording
src/Hangfire.Raft/RaftStorageOptions.cs, README.md, samples/Hangfire.Raft.K8sSample/Program.cs, samples/Hangfire.Raft.Sample/Program.cs
FlushInterval is documented as a public option with background batching semantics, and the README and sample comments are rewritten to describe quorum-based durability and development-only single-node mode.
Cluster submit and dispose flow
src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
RaftStorageCluster removes the serialized flush gate and synchronous durability flush from SubmitAsync, updates the durability comments, and simplifies disposal to drop the removed flush path.
Test coverage cleanup
tests/Hangfire.Raft.Tests/RaftJobStorageTests.cs
The WAL-root comment in RaftJobStorageTests is clarified, and the crash-survival test that depended on disabled background flushing is removed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hop, hop through quorum nights,
the WAL now flushes in gentle lights.
No lone-node tale, no brittle spark,
just background tides and a steadier arc.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the core change: shifting durability from per-node fsync to quorum-based replication.
Description check ✅ Passed The description is directly related to the PR and accurately summarizes the durability refactor and documentation updates.
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 claude/cool-nobel-5074ba

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
README.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}


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

@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 `@README.md`:
- Around line 81-90: The README durability note still implies per-write
flushing, which conflicts with the updated quorum-commit behavior. Update the
later operational note in the documentation to match the same ack semantics
described by the durability section: the write should be described as returning
after replication to a majority and local apply, with flushes happening
asynchronously or on graceful shutdown, not as a requirement for each
acknowledged write. Use the surrounding durability/operational wording in README
to keep the latency and durability story consistent.

In `@src/Hangfire.Raft/Cluster/RaftStorageCluster.cs`:
- Around line 465-470: The shutdown flush in RaftStorageCluster.DisposeAsync is
unbounded because _wal.FlushAsync(CancellationToken.None) can hang forever on a
stalled filesystem. Update the DisposeAsync cleanup path around _wal to use a
bounded/cancellable flush with a short timeout or shutdown token, and keep it
best-effort by logging and swallowing timeout failures rather than blocking
process exit.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec310b5b-211c-407a-9828-6ead67db2d4c

📥 Commits

Reviewing files that changed from the base of the PR and between cda29d6 and ca73566.

📒 Files selected for processing (6)
  • README.md
  • samples/Hangfire.Raft.K8sSample/Program.cs
  • samples/Hangfire.Raft.Sample/Program.cs
  • src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
  • src/Hangfire.Raft/RaftStorageOptions.cs
  • tests/Hangfire.Raft.Tests/RaftJobStorageTests.cs

Comment thread README.md
Comment thread src/Hangfire.Raft/Cluster/RaftStorageCluster.cs Outdated
@msallin
msallin force-pushed the claude/cool-nobel-5074ba branch from ca73566 to c21afab Compare June 24, 2026 09:25

@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 `@src/Hangfire.Raft/Cluster/RaftStorageCluster.cs`:
- Around line 461-466: The Raft storage shutdown path currently only disposes
`_wal`, so `FlushInterval = Timeout.InfiniteTimeSpan` has no real way to persist
buffered entries through the storage API. Update `RaftStorageCluster` to expose
and use an explicit flush path for `WriteAheadLog` (or revise
`RaftStorageOptions`/its docs to remove the “persisted only when explicitly
flushed” promise), and make sure the behavior is consistent with the
`FlushAsync` limitation noted in the teardown logic.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 97a01fdc-5e10-48c2-be06-04b40a929d40

📥 Commits

Reviewing files that changed from the base of the PR and between ca73566 and c21afab.

📒 Files selected for processing (6)
  • README.md
  • samples/Hangfire.Raft.K8sSample/Program.cs
  • samples/Hangfire.Raft.Sample/Program.cs
  • src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
  • src/Hangfire.Raft/RaftStorageOptions.cs
  • tests/Hangfire.Raft.Tests/RaftJobStorageTests.cs
✅ Files skipped from review due to trivial changes (3)
  • samples/Hangfire.Raft.K8sSample/Program.cs
  • samples/Hangfire.Raft.Sample/Program.cs
  • README.md

Comment thread src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
Per the dotNext maintainer (dotnet/dotNext#285),
durability is a cluster property: a write is durable once committed to a quorum,
and a crashed node recovers its unflushed tail from the leader on restart. The
per-write synchronous flush only hardened the handling node's disk, not the
quorum, at the cost of throughput, so remove it.

- Remove the per-write synchronous flush (DurablyFlushAsync and its gate).
  SubmitAsync returns once the entry is committed and applied; the WAL's eager
  background flusher (FlushInterval.Zero) persists it to local disk shortly
  after, without blocking the writer.
- Make FlushInterval a public tuning knob. Zero (default) flushes eagerly per
  commit in the background; a positive value batches fsyncs for throughput at
  the cost of a larger crash window, which a multi-node cluster re-replicates
  from the quorum on restart.
- Document single node as development-only (no fault tolerance) across the
  README, samples, and options; production needs an odd node count.
- Drop the single-node crash-durability test (it asserted a guarantee the
  quorum model no longer makes). Restart durability stays covered by the WAL
  replay and multi-node tests.
@msallin
msallin force-pushed the claude/cool-nobel-5074ba branch from c21afab to 5c2a8cc Compare June 24, 2026 15:16
@msallin
msallin merged commit e066cd4 into main Jun 24, 2026
3 checks passed
@msallin
msallin deleted the claude/cool-nobel-5074ba branch June 24, 2026 19:39
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