Repository navigation
feat(#9399): expose the follower replication-path and unreachable-leader state in GET /api/v1/cluster - #9451
Conversation
…der state in GET /api/v1/cluster - localReplicationPathUnproven (#9013): the in-place-restart state that holds the stale-term reformat back; the follower-stuck-at-stale-term alert carries it in details.replicationPathUnproven and picks its recommendation from it (held, restart by hand vs. self-heals). - localLeaderUnreachableSinceRestart (#8953): tracked by the health monitor - path unproven since the in-place restart while no leader is known or the leader reports entries this node does not hold, for more than 2x the election timeout. New follower-leader-unreachable-since-restart critical alert; the stall alert stands down for it. - OpenAPI ClusterStatus schema declares both fields as required. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (12)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review. 📝 WalkthroughWalkthroughThe change tracks whether a follower’s replication path is proven after an in-place Raft restart. Cluster status reports replication-path and leader-unreachable state. Cluster alerts use these signals to describe stale-term and unreachable-leader conditions. ChangesFollower replication-path diagnostics
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Feature · Severity of issue fixed: Low Sequence Diagram(s)sequenceDiagram
participant HealthMonitor
participant RaftHAServer
participant GetClusterHandler
participant ClusterAlerts
HealthMonitor->>RaftHAServer: Track leader reach after restart
RaftHAServer-->>GetClusterHandler: Provide replication-path state
GetClusterHandler->>ClusterAlerts: Scan using replication-path state
ClusterAlerts-->>GetClusterHandler: Return selected alerts
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The new cluster-status fields and alerts have no identified merge-blocking issue; proceed with normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 21 |
🟢 Coverage 87.91% diff coverage · -5.73% coverage variation
Metric Results Coverage variation ✅ -5.73% coverage variation Diff coverage ✅ 87.91% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (3119a4c) 215829 184073 85.29% Head commit (f6e95a9) 248518 (+32689) 197722 (+13649) 79.56% (-5.73%) 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 (#9451) 91 80 87.91% 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.
Review of PR 9451I read the main-code diff: Overall: the design is clean. The predicate is pure and separate from the stateful tracker, and the hook is defensive (it never propagates, and it resets on any read failure). It sits before the early returns in Points to check
TestsCoverage looks reasonable: a unit test for the tracker and path state,
Per CLAUDE.md, check that none of the new tests assert on raw wall-clock time. They should use the injected clock or Minor
Nothing blocking from my side; point 1 is the one I would settle before merging. |
…arning, drop hard-coded defaults, note the shared clock Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review of #9451Overall this is a solid, well-documented change. It adds two node-level fields ( Good
Suggestions (minor)
Tests Nothing blocking from me. |
… clears the leaderless report Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Review of PR #9451 (diff read only, tests not run). Well-scoped and well-documented; nothing blocking. Items worth a look: (1) the PR description says the new alert is critical, but the code raises critical only when a leader is known and warning when none is, so update the description. (2) On the leaderless arm, localLeaderUnreachableSinceRestart is true on every in-place-restarted node during quorum loss while the alert is only a warning; consider keeping the boolean false there or logging at INFO. (3) leaderReachTracker shares followerStallClock; a neutral name would be less surprising. (4) Note in the tracker Javadoc that observe/reset are single-threaded (sinceMs is a plain long). (5) The two stale-term recommendation strings duplicate much prose; consider shared constants. (6) The IT uses deprecated new URL(String); prefer URI.create(...).toURL() or an existing fixture HTTP helper. (7) Confirm HardcodedTestServerPortsTest stays green with SERVER_LIST in Issue9399ReplicationPathStateTest. (8) The IT adds about grace+2s of waiting; consider @tag("slow") if the grace is large. Positives: thorough tests (predicate, tracker, both arms, grace, resync, closed division, precedence, OpenAPI, real-Ratis idle IT), negligible hot-path cost, no new deps, no new security surface. Items 1 and 2 are the ones I would address before merge. 🤖 Generated with Claude Code |
…deprecated URL constructor Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ReviewI read the diff only. I did not build the PR or run its tests. The design is clean: the tracker is a small pure class with an injectable clock, alert precedence is explicit, and the OpenAPI schema is updated. Findings: 1. PR description and code disagree on severity. The description calls the new alert "critical", but 2. The no-leader arm can fire on healthy or idle nodes. 3. Staleness of 4. Thread-safety is fine but fragile. 5. Test coverage. The tracker predicate and grace, alert precedence, schema required fields, and the idle/clearing IT are covered. Missing is an end-to-end test where the alert actually raises with 6. Minor. The WARNING log text is long. Consider a one-line log and moving the guidance into docs. None of these look blocking. Item 1 is a doc fix, and items 3 and 5 are worth a quick check before merge. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9451 +/- ##
=========================================
Coverage 77.04% 77.04%
- Complexity 4004 4046 +42
=========================================
Files 2131 2132 +1
Lines 215829 215914 +85
Branches 46026 46045 +19
=========================================
+ Hits 166278 166349 +71
- Misses 31795 31804 +9
- Partials 17756 17761 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Closes #9399
Closes #9013
Closes #8953
Summary
A follower whose Raft layer was restarted in place reports
raftState: RUNNING, because the new division is running, even when the leader's appends never reach it (#8898). Before this PR the status document had no machine-readable field for that state. This PR adds two node-level fields toGET /api/v1/clusterand one new alert:localReplicationPathUnproven(HA: expose the held stale-term reformat (dead replication path) as machine-readable state in GET /api/v1/cluster #9013) is true when the node was restarted in place and has taken no replicated entry since, and no newer term with a known leader either. This is the state that makes the health monitor hold the stale-term reformat back (HA: stale-term divergence reformat fires on a follower that has received no append since its restart (dead path, not divergence) #8901). Thefollower-stuck-at-stale-termalert now carriesdetails.replicationPathUnproven, and its recommendation depends on it: "restart this node by hand" when it is true, the self-heal text when it is false. Dashboards can now tell "held" from "will self-heal" without parsing prose.localLeaderUnreachableSinceRestart(HA: a follower the leader cannot reach after an in-place Ratis restart still reports raftState=RUNNING (follow-up of #8900) #8953) is the incident form of the same state. It is set when the path is still unproven while either no leader has made itself known, or the leader reports a commit index past every entry this node holds, and that lasts longer than 2x the effective election timeout max. The health monitor tracks it on every tick with the newHealthTarget.trackLeaderReachSinceRestart()hook, placed next totrackFollowerStall()so it runs before the early returns. The node logs a WARNING when the condition is first reported and an INFO when it clears. The condition raises the new alertfollower-leader-unreachable-since-restart. It is critical when a known leader reports entries this node does not hold, which is direct evidence that its appends are not arriving. It is only a warning when no leader is known, because every node of a cluster that has lost its quorum shows that too. In that case the recommendation first tells you to check the quorum.details.leaderKnownanddetails.unreachableForMscarry the same information as fields.stuck-at-stale-termsuppressesleader-unreachable-since-restart, and that one suppressesstalled-behind-leader. Both new fields are declared as required in the OpenAPIClusterStatusschema.Why not
leaderContactElapsedMs/lastRpcElapsedTimeMs(the signal #8953 suggested first)I read Ratis 3.3
FollowerState.runImpl()andLeaderElection.askForVotes(). A follower that no leader reaches becomes a CANDIDATE after one election timeout. Its pre-vote is rejected, andserver.rejected()turns it back into a follower with a newFollowerStatewhoselastRpcTimestarts at creation. So the figure never grows past one election timeout, and while the node is a candidate it reads-1.RaftHAServer.isReadyForTraffic()also notes that this signal was tried and removed in PR #7605 because a granted PRE_VOTE refreshes it. That is why the new signal uses the in-place-restart baseline (#8901) plus the leader-reported commit index (#7619), which arrives over a follower-to-leader call that does not depend on the broken channel.Why the field is not just
localReplicationPathUnprovenOn an idle cluster a healthy restarted follower receives heartbeats but no entries, so the path stays "unproven" for as long as nobody writes. That is fine as a qualifier on the stale-term alert. Raising an alert on it alone would be a false positive.
Issue8901ReplicationPathAfterInPlaceRestartITnow checks this against real Ratis: after the restart the path is unproven, the reach hooks run for longer than the grace, andlocalLeaderUnreachableSinceRestartstays false.Finding ledger
isReplicationPathUnprovenSinceRestart()intoGetClusterHandlerand add areplicationPathUnprovendetail to the stale-term alertdetails.replicationPathUnprovenplus a recommendation keyed on itlocalReplicationPathUnproven/api/v1/clusterlocalLeaderUnreachableSinceRestart, plus thefollower-leader-unreachable-since-restartalertHealthMonitorsense the issue uses ("theHealthMonitorlifecycle branch cannot see the case"): a debounced, tick-tracked signal with a WARNING log./api/v1/readyand/api/v1/healthare deliberately unchanged, see Known gapsCoverage
GetClusterHandlerwriteslocalReplicationPathUnprovenIssue8901ReplicationPathAfterInPlaceRestartIT(true after the real in-place restart, false after the first entry),GetClusterHandlerIT(false, present on every node)GetClusterHandlerwriteslocalLeaderUnreachableSinceRestartGetClusterHandlerIT,Issue8901ReplicationPathAfterInPlaceRestartIT(idle, no false positive)ClusterAlerts.addStuckAtStaleTermAlertdetails and recommendationIssue9399ReplicationPathStateTest.theStaleTermAlertSaysWhetherTheReformatIsHeld,ClusterAlertsTestfollower-leader-unreachable-since-restartalert and precedenceanUnreachableLeaderRaisesACriticalAlertWithItsDuration,oneAlertPerConditionMostSpecificCauseFirst,noUnreachableLeaderNoAlertRaftHAServer.trackLeaderReachSinceRestart(both arms, grace, clear-on-entry, resync, CLOSED division, never restarted)Issue9399ReplicationPathStateTestHealthMonitor.tick()calls the hook, including on early-return tickstheHealthTickTracksTheLeaderReachEvenOnATickThatReturnsEarlyClusterStatusschemaPluginApiSpecTest,Issue7577ClusterStatusSchemaMatchesTheHandlerTest.theReplicationPathSignalsAreDeclaredAndRequiredClusterAlerts.scan(...)overloadsfalse, -1(no new alert)Issue8342FollowerLocalStallSignalTestKnown gaps:
/api/v1/readyand/api/v1/healthdo not use the new signal. This is on purpose. Readiness already goes to 503 for this follower once the leader's commit runs past the lag threshold (The #7131 readiness gate is dead code: peerCount > 1 && commitIndex < 0 is strictly subsumed by the commitIndex < 0 check on the next line, so the wedged-channel follower still answers Ready #7619behindLeaderCommit). On an idle cluster the follower's data is current, so taking it out of the Service would cost reads and protect nothing. Failing liveness would restart the pod on a signal that, by design, also fires when the whole cluster has no leader. No follow-up was filed because this is a design decision, not a defect. Reopen if you want readiness to consult it.Test plan
mvn -pl ha-raft test -Dtest=Issue9399ReplicationPathStateTest,ClusterAlertsTest,Issue7577ClusterStatusSchemaMatchesTheHandlerTest,Issue8342FollowerLocalStallSignalTest,Issue8901DeadReplicationPathNoReformatTest,HealthMonitorTest,OldRatisServerTerminationTest: 123 tests, 0 failuresmvn -pl ha-raft test -DexcludedGroups=benchmark,slow,vector(whole ha-raft unit suite): 2780 tests, 0 failuresmvn -pl server test -Dtest=PluginApiSpecTest,HardcodedTestServerPortsTest: 29 tests, 0 failuresmvn -pl ha-raft verify -DskipITs=false -Dit.test=Issue8901ReplicationPathAfterInPlaceRestartIT,GetClusterHandlerIT: 7 tests, 0 failuresAdversarial pass
The Phase 1.5 subagent could not be spawned because no subagent tool was available in this run. I did the pass myself instead. That is weaker than an independent reviewer, so I'm noting it here:
leaderReportedCommitIndexfrom before the restart: every value it holds is a commit index some leader really reported. A live path would deliver those entries and prove the path, so a stale value is still valid evidence. Not real.isResyncInProgress()(snapshotDownloadInProgressis part ofLocalResyncState). Not real.heldIndexismax(lastLogIndex, appliedIndex), and during the install the resync exclusion applies. Not real.replicationPathUnprovenis read at request time, while the monitor's hold decision is made at tick time: they can disagree for at most one health tick. The request-time value is the more current of the two. Accepted.Review cycles
warningand carriesdetails.leaderKnown; hard-coded defaults removed from the recommendation; comment on the shared clockURIin the ITURI.create(...).toURL()Deferred items
Skipped or deferred review suggestions, each with its reason:
localLeaderUnreachableSinceRestartpoints readers atdetails.leaderKnown. A separatelocalLeaderUnreachableLeaderKnownfield is a one-line addition if you prefer a field at node level.leaderReportedCommitIndex... confirm it is reset on a leader change or in-place restart" (cycle 4, Bump slf4j-simple.version from 1.7.31 to 1.7.32 #3). Not changed. The field is replaced only by a successful probe, but every value it holds is a commit index some leader really reported, and committed entries are never truncated. A stale value therefore names entries a live path would deliver, which proves the path and clears the report. The one exception is a wholesale Raft-state reset of the whole cluster, which the field's own javadoc already discusses for The #7131 readiness gate is dead code: peerCount > 1 && commitIndex < 0 is strictly subsumed by the commitIndex < 0 check on the next line, so the wedged-channel follower still answers Ready #7619.RaftHAServer's hook with a mocked division (Issue9399ReplicationPathStateTest). A real-Ratis reproduction of the dead path is timing-dependent (HA: after a JVM pause past Ratis's 60s close threshold, an in-place Ratis restart never recovers the follower (appends hit a CLOSED division, then a needless storage reformat) #8898, HA: in-place Ratis restart can leave the leader appending to a CLOSED division (restart verification, leader-side appender rebuild, regression IT) #8900), andIssue8901ReplicationPathAfterInPlaceRestartITdocuments that. The IT is already@Tag("slow").ClusterAlerts.scaninto a parameter record (cycle 2, build support #1). Skipped. The overload chain follows the existing HA: follower stuck at the snapshot index after a snapshot install (stale term), cluster silently loses fault tolerance for ~60 s #8289 and HA: a follower stuck in catch-up at the current term has no follower-local signal (only the leader sees the stall) #8342 pattern, and the refactor would touch every existing call site in the tests.followerStallClock(cycles 1-3). Skipped. A comment at the field now says it is the shared test seam. A rename would change the existingsetFollowerStallClocktest API.catch (Exception)(cycle 2, Bump slf4j-simple.version from 1.7.31 to 1.7.32 #3). Skipped.getDivisionthrows a checkedIOException, and the catch matchesisReadyForTraffic()andtrackFollowerStall().sinceMsvolatile, shared constants for the recommendation strings, shorten the WARNING log (nitpicks). Skipped. The class javadoc states the single-writer contract.Final state
max-cycles-reached(4 of 4). No review flagged anything blocking. The open items are the deferred suggestions above.🤖 Generated with Claude Code
Summary by CodeRabbit