Skip to content

Feat/nodedrain failure domain gate - #387

Open
geoffrey1330 wants to merge 10 commits into
mainfrom
feat/nodedrain-failure-domain-gate
Open

Feat/nodedrain failure domain gate#387
geoffrey1330 wants to merge 10 commits into
mainfrom
feat/nodedrain-failure-domain-gate

Conversation

@geoffrey1330

@geoffrey1330 geoffrey1330 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes; issue-383

Per-failure-domain drain concurrency gate

Fixes #383.

When enableFailureDomains=true, the node drain coordinator now gates concurrent drains by failure domain instead of by node count. Workers in the same failure domain can drain in parallel (they share a fault boundary), while workers from different domains are blocked until an active domain finishes — capped at maxFaultTolerance active domains at a time.

Changes

  • handleDetected: FD-enabled path checks active domains via activeDrainDomains; FD-disabled path keeps the existing node-count gate unchanged
  • workerFailureDomain: reads failure domain from status.nodes[] (populated from backend API) instead of spec, so nodes added outside the operator are handled correctly
  • activeDrainDomains: uses workerFailureDomain to resolve per-node overrides via spec.nodeConfigs[worker].failureDomain
  • Replaced "in_shutdown"/"in_restart" string literals with existing constants to fix lint

Tests added

  • Same-domain parallel allowed
  • Cross-domain gated when active domain count meets maxFaultTolerance
  • FD-disabled falls back to global node-count gate
  • workerFailureDomain reads from status, returns (0, false) for unassigned nodes

wmousa and others added 8 commits August 6, 2026 14:13
…count

  With failure domains enabled, placement guarantees at most one erasure-coding
  chunk per domain, so losing an entire domain at once is already tolerated the
  same way losing a single node is tolerated without FD � the unit
  maxFaultTolerance counts against becomes the domain, not the node.

  handleDetected now counts distinct active failure domains (via
  StorageNodeSet.spec.nodeFailureDomains) instead of raw drain count when FD is
  enabled: a candidate whose domain already has an active drain (planned or a
  pre-existing unhealthy node) may proceed regardless of the raw node count; a
  candidate that would add a new domain is gated on the distinct-domain count.
  Falls back to the existing node-count gate when FD is disabled or a node has
  no domain assignment.
…count

  With failure domains enabled, placement guarantees at most one erasure-coding
  chunk per domain when there are at least ndcs+npcs distinct domains, so
  losing an entire domain at once is already tolerated the same way losing a
  single node is tolerated without FD � the unit maxFaultTolerance counts
  against becomes the domain, not the node.

  handleDetected now counts distinct active failure domains (via
  StorageNodeSet.spec.nodeFailureDomains) instead of raw drain count when FD is
  enabled: a candidate whose domain already has an active drain (planned or a
  pre-existing unhealthy node) may proceed regardless of node count; a
  candidate that would add a new domain is gated on the distinct-domain count
  against maxFaultTolerance.

  When the cluster has fewer domains than ndcs+npcs (meets the npcs+1
  activation minimum but not full one-chunk-per-domain isolation), at least one
  domain necessarily carries more than one stripe chunk, so losing two
  different domains at once can no longer be assumed safe. In that case a
  candidate may still fully drain whichever single domain is already active,
  but opening a second distinct domain falls back to the plain node-count
  budget instead of a second free domain slot.

  Falls back to the existing node-count gate entirely when FD is disabled or a
  node has no domain assignment.
fdDrainGate previously only tracked whether a domain had ANY active
drain (activeDrainDomains, presence-only) and gated a second domain on
raw node count once one was active. Confirmed against the backend
team's stated requirements for 2/3/4-domain 2+2 layouts, that allowed
unsafe combinations (e.g. one node in FD1 plus two more in FD2 on a
2-domain cluster) -- piling onto an active domain isn't free once that
domain has already absorbed more chunks than it can safely re-lose.

Replaced with a per-domain risk budget of npcs: each domain's
contribution to the risk is capped at chunksPerDomain =
ceil((ndcs+npcs)/domainsAvailable) -- the worst-case chunk count any
single domain can hold under placement's even-spread strategy. A domain
already at or above that count has maxed its contribution, so further
nodes in the same domain are free; otherwise the summed capped risk
across all affected domains plus the candidate must stay within npcs.
This mirrors simplyblock_core's _check_ftt_allows_node_removal formula
exactly, so the two stay in lockstep.

activeDrainDomains (presence-only) replaced by activeDrainDomainCounts
(per-domain counts, what the risk formula needs). Added
ParityChunksFromErasureCodingScheme (npcs) alongside the existing
RequiredNodesFromErasureCodingScheme (ndcs+npcs) to source both halves
of the formula from the cluster's erasureCodingScheme.
reconcileActivate fired POST /activate as soon as Spec.Action==Activate
and the cluster UUID resolved, with no check on node count, domain
count, or balance. If the backend correctly refused an under-provisioned
FD cluster (fewer than npcs+2 distinct domains, or unequal per-domain
host counts -- see simplyblock_core's fd_activation_domain_count_violation),
the CR got permanently stuck: failActivate sets ActionStatus.State=Failed,
and nothing in Reconcile/reconcileActivate ever resets ActionStatus away
from Failed back to Running, so the next reconcile falls straight through
to the GET-polling tail and loops forever without ever retrying the POST.

Added a readiness check at the very top of reconcileActivate, before any
ActionStatus mutation: for FD-enabled clusters, aggregate each host's
failure domain across every StorageNodeSet belonging to the cluster
(clusterFailureDomainHosts) and validate domain count/balance
(fdActivationDomainCountViolation, mirroring the Python-side check
exactly). On failure, just requeue -- ActionStatus is left completely
untouched, so there's nothing to get stuck once enough domains show up.
No-op for FD-disabled clusters, confirmed via the existing activate
tests passing unchanged.
@wmousa
wmousa force-pushed the feat/nodedrain-failure-domain-gate branch from 43bde05 to dc47ecb Compare August 6, 2026 12:40
wmousa added 2 commits August 6, 2026 15:05
staticcheck SA1019 flagged the read: Requeue is deprecated in favor of
RequeueAfter. reconcileActivate's init-action step actually returns
Result{Requeue: true} (unrelated production behavior, not changed
here), so check res.IsZero() instead of reading the deprecated field
directly -- still verifies a requeue was requested without referencing
Requeue.
reconcileActivate (StorageCluster controller) already refuses to POST
/activate until fdActivationDomainCountViolation clears, but there is a
second, independent path that fires the same POST: the StorageNodeSet
controller's maybeActivateCluster, triggered whenever
ShouldActivateCluster's online/healthy node count matches the erasure
coding scheme. That check has no notion of failure domains at all, so
on a live 2+2/3-domain deployment it kept firing /activate every
reconcile, and the backend's own defense-in-depth check
(fd_activation_domain_count_violation) synchronously rejected and
reverted it -- observed on the GCP OKD cluster as a repeating
unready -> in_activation -> unready cycle roughly every 7 minutes.

Add the same npcs+2-domain gate to maybeActivateCluster, reusing
clusterFailureDomainHosts/fdActivationDomainCountViolation so the two
call sites stay in lockstep. No-op for FD-disabled clusters.
@noctarius noctarius added this to the 26.4 milestone Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow concurrent node drain within the same failure domain when failure domains are enabled

3 participants