Skip to content

PHOENIX-7973 HA client can adopt a stale (lower-version) ClusterRoleRecord when one endpoint lags the peer - #2589

Open
lokiore wants to merge 3 commits into
apache:PHOENIX-7562-feature-newfrom
lokiore:PHOENIX-7973-crr-reconcile
Open

PHOENIX-7973 HA client can adopt a stale (lower-version) ClusterRoleRecord when one endpoint lags the peer#2589
lokiore wants to merge 3 commits into
apache:PHOENIX-7562-feature-newfrom
lokiore:PHOENIX-7973-crr-reconcile

Conversation

@lokiore

@lokiore lokiore commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Hardens the HA client against adopting a stale, lower-version ClusterRoleRecord (CRR) when one RegionServer endpoint lags its peer. Client-side only; no API/wire change. Base: PHOENIX-7562-feature-new.

  • Fetch reconciliation (getClusterRoleRecordFromEndpoint): now fetches from both cluster endpoints and reconciles via new helper reconcileClusterRoleRecords(r1, r2) — non-UNKNOWN beats UNKNOWN; else higher admin version wins; tie → peer. Previously returned cluster 1 immediately whenever it had no UNKNOWN role.
  • Refresh guard (refreshClusterRoleRecord): new helper shouldApplyRefreshedRecord(current, fetched) rejects a strictly-lower version via isNewerThan(). Equal version is still applied, so autonomous same-version role transitions still take effect.

Why are the changes needed?

CRR admin-version propagation across a cluster's RegionServers is not synchronized and the client picks an endpoint per fetch, so during startup/transition one endpoint can briefly serve a lower version (or UNKNOWN). The old refresh path guarded only with equals() (ignores version), letting a lagging endpoint silently revert the client to a stale view.

Does this PR introduce any user-facing change?

No.

How was this patch tested?

New unit tests in HighAvailabilityGroupTest (no mini-cluster): testReconcileClusterRoleRecords and testShouldApplyRefreshedRecord (reject lower, apply higher, apply same-version-with-changed-roles). spotless:check clean. Adds one endpoint RPC on the refresh path only — not the query hot path.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.8 (1M context))

lokiore and others added 3 commits July 28, 2026 10:07
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…lpers

Condense the method Javadocs, inline comments, and test Javadocs added for
the two-endpoint reconciliation and refresh guard down to the non-obvious
contract (UNKNOWN-not-usable-for-routing, higher-version-wins, same-version
still applied for autonomous transitions, package-private-for-test). No
behavior change; comments only.

Generated-by: Claude Code (Opus 4.8)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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