PHOENIX-7973 HA client can adopt a stale (lower-version) ClusterRoleRecord when one endpoint lags the peer - #2589
Open
lokiore wants to merge 2 commits into
Conversation
…ecord when one endpoint lags the peer getClusterRoleRecordFromEndpoint() queried cluster 1 first and returned it immediately whenever it had no UNKNOWN role, without consulting cluster 2. CRR version propagation across RegionServers is not synchronized, so at startup or during an in-flight admin/failover transition one endpoint can momentarily serve a lower admin version (or an UNKNOWN role) than its peer. In that window the client adopted the staler, lower-version record and silently reverted to an older cluster-role view. The refresh path guards only with ClusterRoleRecord.equals() (which ignores version) and never called the existing isNewerThan() helper, so nothing detected the downgrade. Fix: always fetch the CRR from both cluster endpoints and reconcile via a new package-private static reconcileClusterRoleRecords(): prefer a record without an UNKNOWN role (a known-role record is usable for routing; an UNKNOWN one is not), and within the same category prefer the higher admin version. This is a strict superset of the previous UNKNOWN-only handling and guarantees the client never adopts a CRR older than one a peer already advertises. If the peer endpoint is unreachable, cluster 1's record is used as-is. Adds one endpoint RPC to the CRR refresh path only; CRR is fetched on connect/refresh (cached), not per query, so no meaningful perf impact. Client-side only; no API or wire-format change. Unit-tested via HighAvailabilityGroupTest#testReconcileClusterRoleRecords (higher-version-wins regression guard, order-independence, non-UNKNOWN beats UNKNOWN in both orders, UNKNOWN-vs-UNKNOWN higher-version). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…er version The refresh path applied any non-equals() ClusterRoleRecord fetched from the endpoints, with no version comparison. Because CRR propagation across a cluster's RegionServers is eventually consistent and the client picks an endpoint at (effectively) random per fetch, a lagging endpoint can momentarily serve an older admin version than the client has already applied, silently reverting the client to a stale cluster-role view. Add a shouldApplyRefreshedRecord(current, fetched) guard that keeps the current record when it is strictly newer than the fetched one (equivalently, !current.isNewerThan(fetched)). An equal admin version is intentionally still applied: the admin version only advances on an operator-driven change, so an autonomous state-machine transition changes the cluster roles while keeping the same version, and that legitimate same-version role change must still take effect. Only a strictly lower version is rejected, so a strict '>' guard is deliberately avoided. The decision is factored into a package-private static helper (mirroring shouldCountFailover / reconcileClusterRoleRecords) and unit-tested in HighAvailabilityGroupTest#testShouldApplyRefreshedRecord: (a) reject a strictly lower version, (b) apply a strictly higher version, (c) apply a same-version record with changed roles. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PHOENIX-7973
JIRA: https://issues.apache.org/jira/browse/PHOENIX-7973
Targets the
PHOENIX-7562-feature-new(consistent failover) feature branch. Client-side only, no API or wire-format change.The Phoenix HA client fetches a
ClusterRoleRecord(CRR) from a RegionServer endpoint of each of the two paired clusters. CRR admin-version propagation across a cluster's RegionServers is not synchronized, and the client picks an endpoint at effectively random per fetch, so at startup or during an in-flight transition one endpoint can momentarily serve a lower admin version (or anUNKNOWNrole) than a peer already advertises. This PR hardens the client against adopting such a staler view, in two complementary places.1. Reconcile both endpoints on fetch (
getClusterRoleRecordFromEndpoint)Previously the fetch queried cluster 1 first and returned its record immediately whenever it had no
UNKNOWNrole, without ever consulting cluster 2.It now always fetches the CRR from both cluster endpoints and reconciles them via a new package-private static helper
reconcileClusterRoleRecords(r1, r2):UNKNOWNrole beats one with anUNKNOWNrole (anUNKNOWNrole means that endpoint could not resolve the cluster roles — e.g. a ZooKeeper problem — and is not usable for routing, so it must never win on version alone).versionwins.This is a strict superset of the previous
UNKNOWN-only handling. If the peer endpoint is unreachable, cluster 1's record is used as-is (exception consumed with aWARN).2. Guard the refresh path against version rollback (
refreshClusterRoleRecord)The refresh path applied any non-
equals()fetched record with no version comparison — so a lagging endpoint serving an older admin version could silently revert the client to a stale cluster-role view even after a newer version had already been applied in the same JVM.A new package-private static helper
shouldApplyRefreshedRecord(current, fetched)now gates the apply: the current record is kept whenever it is strictly newer than the fetched one (equivalently!current.isNewerThan(fetched)).Crucially, an equal admin version is still applied. The admin version only advances on an operator-driven change; an autonomous state-machine transition changes the cluster roles while keeping the same version. That legitimate same-version role change must still take effect — so only a strictly lower version is rejected, and a strict
>guard is deliberately avoided (it would strand failover on autonomous transitions).This wires in the existing
ClusterRoleRecord.isNewerThan()comparison, which the refresh path did not previously use.Performance
Adds one endpoint RPC on the CRR refresh path only. CRR is fetched on connect/refresh and cached in
roleRecord; it is not on the per-query hot path, so there is no meaningful performance impact.Testing
Two new unit tests in
HighAvailabilityGroupTest(mirroring the existing package-privateshouldCountFailoverunit-test pattern — no mini-cluster needed):testReconcileClusterRoleRecords: higher version wins when both usable (regression guard); order-independence; non-UNKNOWNbeatsUNKNOWNeven at lower version (both orders);UNKNOWNvsUNKNOWN→ higher version.testShouldApplyRefreshedRecord: (a) reject a strictly lower version (the stale-rollback guard); (b) apply a strictly higher version; (c) apply a same-version record with changed roles (the autonomous-transition safety property a strict>guard would break).Both touched files are
spotless:checkclean.