Repository navigation
feat(#9553): alert and recovery when every voter has quarantined the same database - #9567
Conversation
…same database
Each node now advertises the databases it holds quarantined on the capability
poll every node already runs against every peer. From that, any node can tell
that every voter of the Raft configuration holds a database quarantined, which
leaves no copy a resync could be served from. In that state:
- GET /api/v1/cluster raises a critical "no-healthy-copy-on-any-voter" alert
naming the way out: accept one copy, make that node leader, or restore
- POST /api/v1/cluster/accept-diverged/{database} (and the gRPC twin) is no
longer limited to a sole voter: it is also open on a node whose every voter
reports the database quarantined, so the recommended recovery can run
- a follower no longer retries a targeted resync from a leader that holds the
same database quarantined and refuses to serve it
A peer that does not answer, or runs a build that does not report its
quarantines, is never counted as quarantined.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 11 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (13)
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. Comment |
|
This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again. |
ReviewOverall this is a well-scoped, well-documented change. It reuses the existing capability poll, keeps the new field optional so mixed-version clusters stay on the safe side, and computes the check from memory only. Tests cover the wire format, the pure helpers, the alert and the accept path. Points worth a look
No security problems spotted: accept-diverged is still root-only, and the alert is scoped to visible databases. The only new data on the capability endpoint is database names, which that endpoint already treats as cluster-internal. Nice work. I'd resolve point 1 (or document it) before merge. |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 13 |
🟢 Coverage 87.41% diff coverage · -5.62% coverage variation
Metric Results Coverage variation ✅ -5.62% coverage variation Diff coverage ✅ 87.41% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (169e31e) 221833 189852 85.58% Head commit (c001164) 254582 (+32749) 203568 (+13716) 79.96% (-5.62%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#9567) 143 125 87.41% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
… off a sole voter The capability registry can be one poll behind, so a voter that has just accepted its own copy still read as quarantined and a second accept elsewhere could choose a second copy. The override now asks every other voter, in parallel with one shared deadline, whether it serves the database, and stays closed when one does or cannot be asked. Also corrects the retry comment: the skip lifts once the leader's next capability answer drops the quarantine. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review of PR #9567 (all voters quarantined, #9553)The design is sound. "Unknown is never counted as quarantined" is the right bias for an alert that tells an operator to force-accept a copy. The registry check is a cheap in-memory read, and the live confirmation before an accept closes the stale-poll window. The notes below are what I'd look at before merge. Potential issues
Minor / style
Tests
SecurityThe accept path stays root-only. The new field travels over the existing authenticated route. The alert is scoped to the caller's visible databases. I found no new exposure. |
…t accepts The live voter check narrows the window to two concurrent requests but does not serialize them, so the alert now says to accept on one node only, and the check's javadoc states both that and that it blocks a worker thread for up to one round. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Review of PR 9567 Well-scoped change. Conservative in the right places: an unknown or stale peer is never counted as quarantined, the new JSON field is optional and wire-compatible with older builds, and getDatabasesQuarantinedOnEveryVoter reads memory only. Test coverage is thorough (wire format, registry, decision logic, alert scoping, override, retry filter, live confirmation). Points worth a look
No blocking issues found. I did not build or run the tests locally. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9567 +/- ##
============================================
- Coverage 77.36% 77.36% -0.01%
- Complexity 4165 4228 +63
============================================
Files 2157 2157
Lines 221833 221965 +132
Branches 47521 47565 +44
============================================
+ Hits 171611 171713 +102
Misses 32020 32020
- Partials 18202 18232 +30 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Closes #9553
Summary
When every voter of the cluster quarantined the same database (the #9547 soak: database
chaoson all three nodes in the same second), nothing said so. Each node logged its own SEVERE line and showed its ownlocal-resync-in-progressalert, whose advice (wait, or force a resync) can never work in this state. The followers kept retrying a resync against a leader that answers 503 for a quarantined database. The one override that lifts a quarantine without a resync, accept-diverged (#9449), was refused with 409 because these nodes are not sole voters.Each node now publishes the databases it holds quarantined on the capability poll that every node already runs against every peer (
POST /api/v1/cluster/capabilities, new optionalquarantinedarray, sent only when non-empty). With that:RaftHAServer.getDatabasesQuarantinedOnEveryVoter()lists the databases this node holds quarantined that every other voter, in its last fresh answer, also reports quarantined. It is computed from memory only, so it is cheap enough for every status poll.GET /api/v1/clusterraises a new critical alert,no-healthy-copy-on-any-voter, with the database names (scoped to what the caller may see). The recommendation names the recovery: accept one copy withPOST /api/v1/cluster/accept-diverged/{database}, make that node the leader, or restore from a backup.RaftHAPlugin.acceptDivergedDatabase) is now also allowed when every voter reports the database quarantined. Before lifting, the node also asks every other voter live whether it serves the database (thecopyOfbootstrap-state question, in parallel, one shared 5 s deadline), and refuses if one does or cannot be asked. The capability registry can be one poll behind, so after one node accepts, a second accept elsewhere is refused. Two accepts sent at the same moment can still both pass, and the alert says to accept on one node only.retryUnfilledSnapshotGapleaves out databases that the leader reports holding quarantined. It checks this before taking its throttle slot, so a leader whose quarantine lifts gets retried on the next tick.A peer that did not answer, has a stale answer, or runs a build without the field is never counted as quarantined. "Unknown" never turns into "no usable copy" here, because the alert tells the operator to force-accept a copy.
Note on the issue's premise: the issue says "the cluster view already carries each peer's diverged databases". It did not.
GET /api/v1/clustershowed only the locallocalResync.divergedDatabases, and the capability advertisement only carried aserviceGapboolean (#8665), which does not include quarantines. This PR adds the per-peer information, using the same carrier #8665 used.Test plan
Issue9553AllVotersQuarantinedTest(20 tests, including the live voter check against stub peers that serve, do not serve, or cannot be dialled): wire shape and parsing (including an older peer without the field), registry freshness and staleness, every-voter decision (one healthy voter, unanswered voter, older-build voter, sole voter, healthy local node), alert content and scoping. Caller-level tests driveClusterAlerts.scan,RaftHAPlugin.acceptDivergedDatabase(String, String)andretryUnfilledSnapshotGapthroughFakeRaftHAServerand a real state machineIssue9449AcceptDivergedDatabaseIT(sole voter over HTTP and the remote client): greenPluginApiSpecTest,HardcodedTestServerPortsTest(server): green;grpc/grpcwcompileCoverage
Invariant: when every voter holds a database quarantined, the cluster reports it in one alert with a recovery that can actually be executed, and no follower keeps retrying a resync whose only source is quarantined.
handleUnexpectedApplyError(APPLY_ERROR, UNDECODABLE_LOG_ENTRY), WAL version gap,settleDivergedStateAfterInstall, quarantine restored from diskdivergedDatabases, whichgetQuarantinedDatabaseNames()reads, so detection does not depend on the causeGetClusterHandler->ClusterAlerts.scan(all overloads go through the production one)POST /api/v1/cluster/accept-diverged/{db}and gRPCAcceptDivergedDatabase(both throughRaftHAPlugin.acceptDivergedDatabase)HAServerPlugindocs updatedretryUnfilledSnapshotGap(health tick)triggerDatabaseResyncat the markPOST /api/v1/cluster/resync/{db}handOffLeadershipToResync, #8483)QUARANTINE_HANDOFF_COOLDOWN_MS(one per node per 10 min), which #8483 designed for exactly "a database that EVERY peer quarantines". After an accept on a follower, a transfer is how leadership reaches the accepted copy, so suppressing it would get in the way of the recoverystudio-cluster.jsrendersalertsgenericallyKnown gaps: None filed. Residual risk is below.
Residual risk
Adversarial pass
The orchestrator could not spawn the independent follow-up subagent (no subagent tool available in this run), so this pass was a self-review against the issue text:
Review cycles
34ebafeef9be91aab100RaftHAServer.refuseUnlessNoOtherVoterServesNow) plus tests against stub peers; corrected the retry commentc0011646f9Skipped review items (with rationale)
retryUnfilledSnapshotGap": the skip line goes throughHALog.log(..., HALog.BASIC, ...), which prints only whenarcadedb.ha.logVerbose>= 1 (default 0). It is a debug trace, and the operator-facing signal is the alert.getLivePeers()may include non-voters": it returnsRaftConfiguration.getCurrentPeers(), the voting members. Listeners are a separate role.isSoleVoter()(A single-node Raft cluster that loses a database directory now quarantines it and never becomes ready again #8940, The sole-voter escape added for the forceSnapshot replay was not applied to the other leader-side quarantine, so one apply error on a single-voter cluster leaves the node permanently not-ready with a Raft log that can never be checkpointed #9308) already relies on the same accessor for the same meaning. When the configuration cannot be read it falls back to the declared list, which can only keep the alert off.quarantinedarray; non-string element": the route is root-authenticated, peer to peer, and bound to its author. A malformed element throws insidePeerCapabilityQuery.parse, which the refresh loop catches per peer (catch (final Exception e)->suspendPeer), so one bad answer does not fail the round.(review on PR #...)comment references": kept for consistency with the module's existing convention (for examplereview on PR #7953).Issue7256,Issue7301,Issue7331,Issue7332,Issue7549,Issue8655). Dropping them is unrelated churn.Final state
clean-approval after 3 cycles.
🤖 Generated with Claude Code