Skip to content

fix(raft): correct ambiguous-write classification, prevent reclaimed-job loss, add operational logging#17

Merged
msallin merged 1 commit into
mainfrom
claude/frosty-chaum-80a8ac
Jun 24, 2026
Merged

fix(raft): correct ambiguous-write classification, prevent reclaimed-job loss, add operational logging#17
msallin merged 1 commit into
mainfrom
claude/frosty-chaum-80a8ac

Conversation

@msallin

@msallin msallin commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Functional-correctness + observability hardening for the v1 mission-critical storage, from a full multi-perspective review of the library. Library, tests, and both samples build with 0 warnings (TreatWarningsAsErrors); the full suite (161 tests, incl. 11 new) is green.

Correctness fixes

  • Ambiguous-write classification (RaftStorageCluster.SubmitAsync): the appended flag was set after the hand-off await, so a SubmitTimeout firing during the commit-wait threw OperationCanceledException first and the write was reported as the safe-to-retry "no leader" outcome instead of ambiguous. The flag is now set before the hand-off and reset only on provably-not-appended retryable failures, so the AmbiguousWrites metric and the operator-facing error are accurate (the metric previously under-counted the most common timeout case).
  • Silent job loss on maintenance (RaftStore.RunMaintenance): a job whose ExpireAt elapsed while it was held under a fetch lease was protected during the live-lease pass, but once the stale lease was reclaimed and re-enqueued its expiry was not cleared, so the next maintenance pass evicted it with zero executions and no log/metric. The reclaim and RequeueFetchedOp now clear ExpireAt (a re-enqueued job is runnable again). Covered by new cross-pass tests; determinism preserved.

Observability (warn/error so problems are visible)

  • Leader logs + meters stale-fetch reclaims via a deterministic MaintenanceSummary (covers the crashed-worker double-run case a live worker can't observe) and warns on dropped orphaned queue entries.
  • SubmitAsync fails fast and warns when the local state machine has faulted, instead of blocking each write for the full SubmitTimeout.
  • Logs for: stranded ambiguous fetches, dispose-time requeue failures, un-loadable job invocation data, snapshot restore, node startup decisions (cold-start vs restart, replay, seed), and a configurable ReadStalenessWarningThreshold for a lagging follower.

Configuration robustness

RaftStorageOptions.Validate() (called at startup) rejects, with named errors instead of raw framework exceptions: inverted election timeouts, non-positive SnapshotInterval/lease timeouts, RpcPortOffset port overflow, port 0, empty bracketed IPv6 host, and deduplicates Members.

Tests

+11 tests (3 store, 8 options); full deterministic + e2e cluster + forwarding suites pass locally.

🤖 Generated with Claude Code

…job loss, add operational logging

Functional correctness:
- SubmitAsync: mark a command appended before the hand-off so a SubmitTimeout
  firing during the commit-wait is surfaced as ambiguous (accurate
  AmbiguousWrites metric and an actionable operator error) rather than as a
  safe-to-retry "no leader" outcome; reset the flag only on provably
  not-appended retryable failures.
- RunMaintenance: clear ExpireAt when a fetched job is re-enqueued (stale-lease
  reclaim and RequeueFetchedOp). A job whose expiry elapsed while it was fetched
  was protected during the live-lease pass but was then silently evicted on the
  next maintenance pass with zero executions.

Observability:
- The leader logs and meters stale-fetch reclaims via a deterministic
  MaintenanceSummary (the crashed-worker double-run case that previously only a
  live worker could observe) and warns on dropped orphaned queue entries.
- SubmitAsync fails fast and warns when the local state machine has faulted.
- Log stranded ambiguous fetches, dispose-time requeue failures, un-loadable
  job invocation data, snapshot restore, node startup decisions, and a
  configurable read-staleness threshold for a lagging follower.

Configuration robustness:
- RaftStorageOptions.Validate() at startup rejects inverted election timeouts,
  non-positive SnapshotInterval/lease timeouts, RpcPortOffset port overflow,
  port 0 and empty bracketed IPv6 hosts, and deduplicates Members.

Adds 11 tests; full suite (161) and both samples build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds configuration validation, startup and restore logging, maintenance summary tracking, and fetch/job-data warning logs across the Raft storage path.

Changes

Raft storage observability, validation, and maintenance

Layer / File(s) Summary
Configuration validation and startup checks
src/Hangfire.Raft/RaftStorageOptions.cs, src/Hangfire.Raft/RaftJobStorage.cs, tests/Hangfire.Raft.Tests/RaftStorageOptionsTests.cs
Adds ReadStalenessWarningThreshold, deduplicates cluster members, tightens endpoint and forwarding-port validation, and calls Validate() before cluster startup; tests cover the new parsing and validation paths.
Startup and restore logs
src/Hangfire.Raft/Cluster/HangfireStateMachine.cs, src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
Adds logs for snapshot restore details, cold-start versus resume decisions, restored WAL indices, node startup, and genesis membership seeding.
Maintenance summary and expiry handling
src/Hangfire.Raft/State/RaftStore.cs, tests/Hangfire.Raft.Tests/RaftStoreTests.cs
RunMaintenance now returns MaintenanceSummary, counts orphaned queue entries, expired locks, stale reclaims, and expired collections, and clears expiry when fetched jobs are requeued; tests cover reclaim, requeue, and eviction counts.
Submit handoff and maintenance loop
src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
SubmitAsync now rejects faulted local state, marks commands appended before leader forwarding, adjusts ambiguous and retryable handling, and the maintenance loop checks stale reads and logs returned summaries.
Fetch lifecycle warnings
src/Hangfire.Raft/RaftStorageConnection.cs, src/Hangfire.Raft/RaftFetchedJob.cs
FetchNextJob logs and retries on unconfirmed fetch submissions, GetJobData warns on deserialization failures, and RaftFetchedJob updates reclaim and dispose warnings.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RaftStorageCluster
  participant Leader
  Client->>RaftStorageCluster: SubmitAsync(command)
  RaftStorageCluster->>Leader: forward after appended=true
  Leader-->>RaftStorageCluster: result or exception
  RaftStorageCluster-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • msallin/hangfire-raft#7: Shares the HangfireStateMachine, RaftStorageCluster, and RaftStore paths updated here.
  • msallin/hangfire-raft#16: Also changes RaftStorageCluster.SubmitAsync in src/Hangfire.Raft/Cluster/RaftStorageCluster.cs.

Poem

A rabbit hopped through logs at dawn,
To count the stale reads, then hop on.
The leases blinked, the queues stayed bright,
Maintenance hummed in soft moonlight.
With one tiny thump, the cluster smiled. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.43% 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 captures the main fixes: ambiguous writes, reclaimed-job loss, and new operational logging.
Description check ✅ Passed The description is clearly aligned with the PR’s correctness, observability, and validation changes.
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/frosty-chaum-80a8ac

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.

Caution

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

⚠️ Outside diff range comments (1)
src/Hangfire.Raft/State/RaftStore.cs (1)

335-353: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don’t gate orphan cleanup on same-pass evictions.

EnqueueOp can append a job id without a _jobs guard, so a missing-job queue entry can already exist before maintenance runs. With the evictedJobs.Count > 0 gate, those entries are neither removed nor reflected in MaintenanceSummary unless another job happens to expire in the same pass, leaving queue counts/metrics stale.

Proposed fix
 var orphanedQueueEntries = 0;
-if (evictedJobs.Count > 0)
+if (_queues.Count > 0)
 {
     foreach (var queue in _queues.Values)
     {
         var node = queue.First;
🤖 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 `@src/Hangfire.Raft/State/RaftStore.cs` around lines 335 - 353, The orphan
cleanup in RaftStore’s maintenance path is incorrectly gated behind
evictedJobs.Count > 0, which leaves queue entries added by EnqueueOp without a
matching _jobs record unprocessed when no jobs expire in the same pass. Remove
that dependency so the queue scan and orphanedQueueEntries removal always run
during maintenance, and ensure MaintenanceSummary is updated from the actual
cleanup count regardless of whether any evictions occurred.
🧹 Nitpick comments (1)
tests/Hangfire.Raft.Tests/RaftStoreTests.cs (1)

760-762: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the rest of the new maintenance summary contract.

This only asserts EvictedJobs and StaleFetchesReclaimed; the PR also added OrphanedQueueEntriesRemoved, ExpiredLocksReleased, and ExpiredCollections. Add assertions or companion tests so the new logging/metrics fields cannot silently regress.

🤖 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 `@tests/Hangfire.Raft.Tests/RaftStoreTests.cs` around lines 760 - 762, The
MaintenanceSummary coverage in RaftStoreTests is incomplete because it only
checks EvictedJobs and StaleFetchesReclaimed; extend the existing maintenance
test around Apply and MaintenanceOp to assert the new MaintenanceSummary fields
OrphanedQueueEntriesRemoved, ExpiredLocksReleased, and ExpiredCollections as
well. If the current scenario cannot exercise all fields, add companion tests in
RaftStoreTests that drive MaintenanceSummary through the relevant maintenance
paths so these contract values are explicitly validated and can’t regress
silently.
🤖 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 `@src/Hangfire.Raft/State/RaftStore.cs`:
- Around line 335-353: The orphan cleanup in RaftStore’s maintenance path is
incorrectly gated behind evictedJobs.Count > 0, which leaves queue entries added
by EnqueueOp without a matching _jobs record unprocessed when no jobs expire in
the same pass. Remove that dependency so the queue scan and orphanedQueueEntries
removal always run during maintenance, and ensure MaintenanceSummary is updated
from the actual cleanup count regardless of whether any evictions occurred.

---

Nitpick comments:
In `@tests/Hangfire.Raft.Tests/RaftStoreTests.cs`:
- Around line 760-762: The MaintenanceSummary coverage in RaftStoreTests is
incomplete because it only checks EvictedJobs and StaleFetchesReclaimed; extend
the existing maintenance test around Apply and MaintenanceOp to assert the new
MaintenanceSummary fields OrphanedQueueEntriesRemoved, ExpiredLocksReleased, and
ExpiredCollections as well. If the current scenario cannot exercise all fields,
add companion tests in RaftStoreTests that drive MaintenanceSummary through the
relevant maintenance paths so these contract values are explicitly validated and
can’t regress silently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aff8172-d0d5-4bd6-a2d7-56b71cebe3c6

📥 Commits

Reviewing files that changed from the base of the PR and between e066cd4 and 15f8600.

📒 Files selected for processing (9)
  • src/Hangfire.Raft/Cluster/HangfireStateMachine.cs
  • src/Hangfire.Raft/Cluster/RaftStorageCluster.cs
  • src/Hangfire.Raft/RaftFetchedJob.cs
  • src/Hangfire.Raft/RaftJobStorage.cs
  • src/Hangfire.Raft/RaftStorageConnection.cs
  • src/Hangfire.Raft/RaftStorageOptions.cs
  • src/Hangfire.Raft/State/RaftStore.cs
  • tests/Hangfire.Raft.Tests/RaftStorageOptionsTests.cs
  • tests/Hangfire.Raft.Tests/RaftStoreTests.cs

@msallin
msallin merged commit 05bc0c3 into main Jun 24, 2026
3 checks passed
@msallin
msallin deleted the claude/frosty-chaum-80a8ac branch June 24, 2026 21:04
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