Skip to content

PHOENIX-7990 Reconcile convergent CAS race in setHAGroupStatusIfNeeded to avoid RS aborts - #2604

Open
tkhurana wants to merge 1 commit into
apache:PHOENIX-7562-feature-newfrom
tkhurana:PHOENIX-7990
Open

PHOENIX-7990 Reconcile convergent CAS race in setHAGroupStatusIfNeeded to avoid RS aborts#2604
tkhurana wants to merge 1 commit into
apache:PHOENIX-7562-feature-newfrom
tkhurana:PHOENIX-7990

Conversation

@tkhurana

Copy link
Copy Markdown
Contributor

When >=2 co-active RegionServers drive the shared HA-status znode to the same target state, each does an optimistic CAS whose expected version comes from the watch-lagged local cache. The first writer wins; the others were aborting the RegionServer even though the shared status was already at the target. This hits the peer-degrade path (ACTIVE_IN_SYNC -> ACTIVE_NOT_IN_SYNC) and, more commonly, the forwarders racing ACTIVE_NOT_IN_SYNC -> ACTIVE_IN_SYNC.

The two convergent losers differ by whether the winner's write reached the loser's cache before it fired, and each is reconciled from a source that is authoritative for its path -- never the watch-lagged cache used as a false-success signal:

Cache-stale loser (client, HAGroupStoreClient.setHAGroupStatusIfNeeded): the CAS is attempted and loses stale; the loop re-reads fresh from ZK and retries, bounded by SET_HA_GROUP_STATUS_MAX_ATTEMPTS. Move 'attempt > 1 && current == target' no-op ahead of validateTransitionAndGetWaitTime so a converged non-self-transitionable target (e.g. ACTIVE_IN_SYNC) returns a no-op success from the fresh re-read instead of throwing InvalidClusterRoleTransitionException on the X -> X self-transition. The first attempt never short-circuits, so the periodic STORE_AND_FORWARD heartbeat still
writes its znode mtime bump that gates SYNC promotion.

Watch-won loser (server, HAGroupStoreManager.setHAGroupStatusToSync): the winner's write already propagated to the cache, so validate rejects the ACTIVE_IN_SYNC -> ACTIVE_IN_SYNC self-transition before any CAS is attempted -- no stale-version exception is ever thrown, so the client cannot catch it. Swallow the InvalidClusterRoleTransitionException as a no-op success, guarded on isStateAlreadyUpdated(current == target) so a genuinely invalid transition still propagates. The ACTIVE_IN_SYNC local listener in ReplicationLogGroup already drives the loser to the correct end state (resume rotation, SYNC_AND_FORWARD -> SYNC), so the group-level goal is met and the loser need not retry its own write.

…d to avoid RS aborts

When >=2 co-active RegionServers drive the shared HA-status znode to the same
target state, each does an optimistic CAS whose expected version comes from the
watch-lagged local cache. The first writer wins; the others were aborting the
RegionServer even though the shared status was already at the target. This hits
the peer-degrade path (ACTIVE_IN_SYNC -> ACTIVE_NOT_IN_SYNC) and, more commonly,
the forwarders racing ACTIVE_NOT_IN_SYNC -> ACTIVE_IN_SYNC.

The two convergent losers differ by whether the winner's write reached the loser's
cache before it fired, and each is reconciled from a source that is authoritative
for its path -- never the watch-lagged cache used as a false-success signal:

  Cache-stale loser (client, HAGroupStoreClient.setHAGroupStatusIfNeeded): the CAS
  is attempted and loses stale; the loop re-reads fresh from ZK and retries,
  bounded by SET_HA_GROUP_STATUS_MAX_ATTEMPTS. Move the existing
  'attempt > 1 && current == target' no-op ahead of validateTransitionAndGetWaitTime
  so a converged non-self-transitionable target (e.g. ACTIVE_IN_SYNC) returns a
  no-op success from the fresh re-read instead of throwing
  InvalidClusterRoleTransitionException on the X -> X self-transition. The first
  attempt never short-circuits, so the periodic STORE_AND_FORWARD heartbeat still
  writes its znode mtime bump that gates SYNC promotion.

  Watch-won loser (server, HAGroupStoreManager.setHAGroupStatusToSync): the winner's
  write already propagated to the cache, so validate rejects the ACTIVE_IN_SYNC ->
  ACTIVE_IN_SYNC self-transition before any CAS is attempted -- no stale-version
  exception is ever thrown, so the client cannot catch it. Swallow the
  InvalidClusterRoleTransitionException as a no-op success, guarded on
  isStateAlreadyUpdated(current == target) so a genuinely invalid transition still
  propagates. The ACTIVE_IN_SYNC local listener in ReplicationLogGroup already drives
  the loser to the correct end state (resume rotation, SYNC_AND_FORWARD -> SYNC), so
  the group-level goal is met and the loser need not retry its own write.

Add HAGroupStoreManagerIT#testSetHAGroupStatusToSyncConvergentRaceIsNoOp covering
the watch-won path: with the record already at ACTIVE_IN_SYNC, setHAGroupStatusToSync
returns 0L with no ZK write instead of throwing. The existing HAGroupStoreClientIT
convergent-race and heartbeat cases continue to cover the cache-stale path.

Follow-up (review): add HAGroupStoreClientIT#
testSetHAGroupStatusIfNeededConvergentRaceNonSelfTransitionableTarget, a subcase-A
regression that converges on a non-self-transitionable target (STANDBY ->
STANDBY_TO_ACTIVE): the existing convergent test converges on ACTIVE_NOT_IN_SYNC (the
lone self-transitionable state) and so passes even without the reorder, whereas this
one throws InvalidClusterRoleTransitionException if the no-op short-circuit is not moved
ahead of validate. Also raise the ReplicationLogDiscoveryForwarder swallowed
setHAGroupStatusToSync failure from INFO to WARN: convergent races now reconcile to a
no-op success, so a throw there is a genuine anomaly (retry-exhausted contention or a
truly invalid transition) and must be visible.
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