8000 feat(components): hold a fenced bridge in cold standby by VLukiantsev · Pull Request #1236 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

feat(components): hold a fenced bridge in cold standby - #1236

Merged
VLukiantsev merged 5 commits into
mainfrom
feat/components-bridge-cluster-fence-b548436300
Oct 19, 2026
Merged

VLukiantsev merged 5 commits into
mainfrom
feat/components-bridge-cluster-fence-b548436300

Conversation

@VLukiantsev
@VLukiantsev VLukiantsev commented Oct 13, 2026 •
Copy link
Copy Markdown
Collaborator

Summary

A fenced xmc-bridge now enters cold standby: when CMS reports that another cluster holds the ownership fence (or this cluster's epoch regressed), it performs no CMS writes and no Helm actuation or teardown for non-deleting CRs, reports SourceConflict on its Component CRs, and re-enqueues every Component when the fence state changes. Deleting a CR in a fenced cluster is handled in #1240 (sourced ExecuteDeletion, StaleDeletion Event, local teardown only). The cluster declaration itself is the layer below.

Motivation & Context

With the CMS fence in place (#1234/#1235) and the bridge declaring its epoch (previous layer), the bridge must stop writing when another cluster holds the fence, so a restored cluster or a DR standby cannot fight the active writer.

Key Changes

  • ClusterFence tracks the declaration outcome (ClusterFenced / ClusterEpochRegressed or active). The CMS call runs outside the fence mutex and stale (out-of-order) results are dropped, so Check never waits on an in-flight RPC; Check declares first if no declaration succeeded yet. Changes() signals each transition.
  • WithClusterFence gates reconcilePass: fenced cold standby reports SourceConflict=True/ClusterFenced, Ready=False, Stalled=True, RequeueAfter 15m; health projection only, no CMS writes and no Helm actuation or teardown.
  • The Component controller watches ClusterFence.Changes() through a channel source and enqueues all Components of its namespace on a fence transition.
  • extractErrorInfo: reads the components-domain ErrorInfo of a wrapped FailedPrecondition and keeps the CMS message.
  • ClusterFence logs each fence status transition, once on the first declaration and once per change (never per heartbeat): Error when the cluster is fenced out (cold standby) and Info when it holds the fence. Without it a fenced bridge left no trace in its logs.
  • ClusterFence.Check shares one first declaration across concurrent reconcile workers (singleflight), detached from any single caller's cancellation and bounded by DeclareTimeout; a waiter whose context ends returns at once without cancelling the shared declaration.
  • wasComponentPreviouslyActive no longer treats a SourceConflict Ready reason as never actuated: the fence can engage after actuation, so disabling a component after unfencing still uninstalls its workload.
  • Tests: cold standby against an in-process CMS (fenced standby on enabled and disabled components, regressed epoch), transient declaration errors, Start and transition notifications and logs (first declaration and each change), a shared first declaration with a cancelled waiter, stale declarations, and a fenced-standby envtest (fence change re-enqueues and recovers the CR).

Stack Position

Assumptions

  • Cold standby holds teardown of non-deleting CRs — a fenced cluster must not delete workloads it may later take over — and until feat(components): enforce deletion ownership and fenced CR cleanup #1240 a CR deleted in a fenced cluster is still deleted unsourced (feat(components): enforce deletion ownership and fenced CR cleanup #1240 sources ExecuteDeletion, so CMS keeps the active owner's record).
  • CMS and the bridge upgrade together from one chart release — otherwise a bridge could call DeclareClusterEpoch on a CMS that lacks it, and its controllers stay gated until CMS is upgraded.
  • The live GKE evidence still applies to this revision — it was collected before the last rebases onto main and before the split — if wrong, the checkpoint must be re-run before merge.

Verification & Testing

  • Commands Executed: bazel test //pkg/components/... //cmd/xmc_bridge/... //cmd/experiment_state_server/... //cmd/actuator/... //test/e2e:kind_3p_harness_unit_test and the //test/helm py tests at this layer's tip; golangci-lint run (v1.64.6); gofmt -l.
  • Coverage & Test Status: all pass, including the fenced-standby envtest.
  • KinD E2E: //test/e2e:components_lifecycle_test passed with the whole stack applied.
  • Diff size (tools/analyze_diff_size.py): 251 prod / 529 test / 780 total / 0 doc lines (was 483 / 589 before the split).

Live Deployment & Manual Testing

Related Issues / Bugs

Bug: 548436300

TAG=agy
CONV=d833b0e6-4821-48f6-8576-776d53f55a5c


Stack created with GitHub Stacks CLI • Give Feedback 💬

@VLukiantsev
VLukiantsev added this pull request to stack #1245 October 13, 2026 07:27
@gemini-code-assist gemini-code-assist Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a cluster fencing mechanism (ClusterFence) to the xmc_bridge component, allowing it to declare cluster ownership epochs to CMS and gate component reconciliations based on active fence status. The changes include new configuration options, RBAC permissions to read the kube-system namespace UID, and comprehensive tests. The review feedback highlights several critical issues: a compilation error in cluster_fence_test.go due to an undefined variable (cmsClient instead of cmsMock), potential data races on f.identity in Identity() and Start(), a potential deadlock in declareLocked if mutex locking is added to Identity(), and a naming conflict risk for cluster-scoped RBAC resources if multiple instances of the Helm chart are installed.

Comment thread pkg/components/bridge/cluster_fence.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
@VLukiantsev
Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a cluster fencing mechanism (ClusterFence) to xmc_bridge to manage active/standby cluster ownership epochs declared to CMS, adding new configuration options, RBAC permissions to read the kube-system namespace UID, and gating reconciliations on holding the active fence. Feedback on these changes suggests refactoring ClusterFence.Declare and Check to execute the blocking gRPC call outside of the mutex lock to avoid blocking concurrent checks. Additionally, the reviewer recommends providing a --cluster-uid flag/env var fallback for restricted Kubernetes environments where reading the kube-system namespace is restricted, and using the standard status.FromError helper for cleaner gRPC error extraction.

Comment thread pkg/components/bridge/cluster_fence.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
Comment thread cmd/xmc_bridge/main.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
@VLukiantsev
8000 VLukiantsev commented Oct 13, 2026
Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a cluster fencing mechanism to the XMC Bridge to coordinate active and standby clusters. It adds configuration options for cluster name and ownership epoch, reads the cluster's unique identity from the kube-system namespace UID, and implements a periodic epoch declaration to the CMS. The reconciler is updated to gate active reconciliations on holding the active CMS ownership fence, transitioning components to a SourceConflict state when fenced. The review feedback points out a logical issue in the ClusterFence's Declare method where transient error checks are performed before checking for stale sequences, which could cause obsolete errors from slow declarations to prematurely trigger retries.

Comment thread pkg/components/bridge/cluster_fence.go Resolved
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch from ee159ab to 6fcf6f4 Compare October 13, 2026 15:09
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch 2 times, most recently from 006038a to d4f4272 Compare October 14, 2026 09:49
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch 2 times, most recently from 0aa86d0 to ff87d20 Compare October 14, 2026 11:58
@VLukiantsev VLukiantsev changed the title feat(components): add bridge cluster identity declaration and cold-standby fence feat(components): hold a fenced bridge in cold standby Oct 14, 2026
@VLukiantsev
VLukiantsev removed this pull request from stack #1245 October 14, 2026 13:31
@VLukiantsev
VLukiantsev changed the base branch from feat/components-ownership-fence-enforcement-b548436300 to feat/components-bridge-cluster-declaration-b548436300 October 14, 2026 13:31
@VLukiantsev
VLukiantsev added this pull request to stack #1277 October 14, 2026 13:32
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch from ff87d20 to 071cf71 Compare October 14, 2026 13:54
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch 4 times, most recently from 9b07bc6 to ddba095 Compare October 15, 2026 08:40
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch from a116869 to d6ed91d Compare October 16, 2026 11:51
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch 2 times, most recently from e6d4e26 to ff3e5a8 Compare October 16, 2026 13:09
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch 2 times, most recently from 5956ef0 to 3d45036 Compare October 16, 2026 13:45
Comment thread pkg/components/bridge/lifecycle.go Resolved
Comment thread pkg/components/bridge/reconciler.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
Comment thread pkg/components/bridge/reconciler.go Resolved
Comment thread pkg/components/bridge/cluster_fence.go Resolved
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch from 8a4e5aa to 7e2ae2f Compare October 16, 2026 15:16
@VLukiantsev
VLukiantsev marked this pull request as ready for review October 16, 2026 15:25
@github-actions
github-actions Bot requested a review from janzak-dev October 16, 2026 15:25
Base automatically changed from feat/components-bridge-cluster-declaration-b548436300 to main October 16, 2026 15:27
@VLukiantsev
VLukiantsev force-pushed the feat/components-bridge-cluster-fence-b548436300 branch from 7e2ae2f to eff3991 Compare October 16, 2026 15:27
janzak-dev
janzak-dev previously approved these changes Oct 19, 2026
Comment thread pkg/components/bridge/cluster_fence.go Resolved
@AntonTyb
Copy link
Copy Markdown
Collaborator

@VLukiantsev - can you share the design doc reference that describes this "fence" mechanism?

@VLukiantsev
Copy link
Copy Markdown
Collaborator Author

@AntonTyb this is a follow-up to the AI test of the implemented component stack. This scenario was missed in the design doc

The previous layer makes the bridge declare its cluster epoch to CMS.
This layer makes it honour the answer, so a cluster that lost the
ownership fence stops writing:

- ClusterFence tracks the declaration outcome: whether this cluster
  holds the fence, or is fenced (CLUSTER_FENCED) or behind
  (CLUSTER_EPOCH_REGRESSED). Check answers from the last applied status
  (declaring first if no declaration succeeded yet) without blocking on
  an in-flight CMS call, a sequence number keeps a slow declaration
  from overwriting a newer one, and Changes signals each transition.
- reconcilePass consults the fence (WithClusterFence) before any CMS
  write or Helm actuation. A fenced Component reports
  SourceConflict=True (ClusterFenced / ClusterEpochRegressed),
  Ready=False and Stalled=True, keeps no status.access, and is
  rechecked every 15 minutes.
- The Component controller watches the fence's Changes channel and
  re-enqueues every Component of its namespace when the fence state
  transitions, so a takeover or failback takes effect immediately.
- extractErrorInfo reads the components-domain ErrorInfo of a wrapped
  FailedPrecondition and keeps the CMS message.

Folds the review fixes of this layer: bounded heartbeats and
transition coverage, declaring outside the fence mutex, ignoring
obsolete declaration failures, keeping the CMS message of a wrapped
ErrorInfo, keeping ParseConfig under the gocyclo limit, and the CMS
authorizer in the tests this layer adds.

Tests: cold standby against an in-process CMS (fenced standby, enabled
and disabled, and a regressed epoch), transient declaration errors,
Start and transition notifications, stale declarations, and an envtest
of a fenced standby that takes over after an epoch bump.

Bug: 548436300
TAG=agy
CONV=d833b0e6-4821-48f6-8576-776d53f55a5c
…in wasComponentPreviouslyActive

Bug: 548436300

TAG=agy

CONV=d833b0e6-4821-48f6-8576-776d53f55a5c
ClusterFence logs the status a declaration moves the cluster into: an
error when it is fenced out of CMS (cold standby) and an info line when
it holds the fence, once on the first declaration and once per change,
never on an unchanged heartbeat. A fenced bridge was otherwise silent.

Comment link: #1273 (comment)

Bug: 548436300
TAG=agy
CONV=d833b0e6-4821-48f6-8576-776d53f55a5c
… bound fence checks

- wasComponentPreviouslyActive no longer treats a SourceConflict Ready
  reason as never actuated: the fence or an ownership conflict can
  engage after actuation, so disabling a component after unfencing
  still uninstalls a workload that ran before the fence.
- ClusterFence.Check shares one first declaration across concurrent
  callers (singleflight), detached from any single caller's
  cancellation and bounded by DeclareTimeout.
- Explain why deletion runs before the fence check, with a TODO for the
  sourced deletion in a later layer, and restore the NewReconciler
  parameter docs.

Comment links:
#1236 (comment)
#1236 (comment)
#1236 (comment)
#1236 (comment)

Bug: 548436300
TAG=agy
CONV=d833b0e6-4821-48f6-8576-776d53f55a5c
…caller's context ends

ClusterFence.Check shares the first DeclareClusterEpoch across reconcile
workers through singleflight. Do blocked every waiter until the shared
call returned, so a worker whose context was cancelled (leader loss or
shutdown) could still wait up to DeclareTimeout. declareShared now uses
DoChan and returns as soon as the caller's context ends; the detached,
DeclareTimeout-bounded declaration keeps running for the other waiters.
DoChan's result channel is buffered, so an abandoned waiter never blocks
the shared call.

Tests:
- TestClusterFence_CheckSharesOneDeclaration: a cancelled caller returns
  while the shared CMS call is still in flight, and later callers still
  share that single call. The scripted CMS now fails a call whose
  context ended, so losing context.WithoutCancel is caught too.
- TestClusterFence_TransitionNotifications: assert the first
  declaration's log line (Info when active, Error when fenced), not only
  the second's.

Comment link: #1236 (comment)
Comment link: #1236 (comment)

Bug: 548436300
TAG=agy
CONV=d833b0e6-4821-48f6-8576-776d53f55a5c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority::2-high top of mind. should not wait.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

0