Skip to content

fix: prevent pool burst creation, zombie churn, and auto-cleanup - #1059

Merged
openshift-merge-bot[bot] merged 7 commits into
openshift:masterfrom
BATMAN-JD:BATMAN-JD/fix-pool-satisfaction-and-burst
Jul 31, 2026
Merged

fix: prevent pool burst creation, zombie churn, and auto-cleanup#1059
openshift-merge-bot[bot] merged 7 commits into
openshift:masterfrom
BATMAN-JD:BATMAN-JD/fix-pool-satisfaction-and-burst

Conversation

@BATMAN-JD

@BATMAN-JD BATMAN-JD commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

The pool controller had several issues causing staging instability:

  1. Zombie churn cycle: Accounts that hit the AWS account limit were left in NoState forever, requeueing every 5 minutes. The pool counted these as "unclaimed" so it thought the pool was satisfied, but they would never provision — blocking new healthy accounts from being created.

  2. Burst creation: The pool created all needed Account CRs in rapid succession via the ownership watch re-reconcile, causing burst CreateAccount API calls that overwhelmed the account controller.

  3. Pending deletion miscounting: Accounts pending deletion were still counted as "unclaimed", inflating the pool satisfaction check and preventing replacement accounts from being created.

  4. 958 zombie Account CRs in staging: Failed, pool-owned Account CRs with no AWS account ID accumulated since 2022. They are irrecoverable but inflate the Account CR list, adding overhead to every pool reconcile.

Changes

Commit: cc94dba — Prevent pool burst creation and zombie churn cycle

  • Pool satisfaction now uses IsPendingFirstProcessing() instead of counting all unclaimed accounts, excluding stuck/zombie CRs
  • Pool satisfaction check uses available + progressing + pending instead of raw unclaimed count
  • 30-second delay between Account CR creations prevents burst API calls

Commit: 01c5c33 — Use Condition instead of failing accounts blocked by AWS limit

  • Accounts blocked by the AWS limit get an observable AccountPending Condition (reason: AWSAccountLimitReached) instead of silently requeueing
  • Sets a Condition, not a State — this is the load-bearing safety decision. Setting State would strand the account forever (never picked up for creation once limit clears, never reaped). Keeping State empty preserves the recovery path
  • The account controller requeues every 5 minutes and will proceed normally once headroom opens up

Commit: d059524 — Exclude accounts pending deletion from pending count

  • Accounts with a deletion timestamp are no longer counted as "unclaimed" in pool satisfaction
  • Ensures the pool controller creates replacement accounts promptly during reuse cleanup

Commit: 612989a — Garbage-collect zombie Account CRs on reconcile

  • On reconcile, Failed Account CRs with no AWS account ID and pool ownership are automatically deleted
  • Finalizers are stripped first to prevent the account controller's deletion path from trying STS assume role into a non-existent account
  • On deploy to staging, this will automatically clean up all ~958 existing zombie CRs

Pool-level limit gate (in cc94dba)

  • Pool controller checks accountCount >= limit before creating new Account CRs
  • If at the limit, pauses and requeues after 5 minutes instead of creating CRs that would sit in NoState
  • This is the missing cap that prevents unbounded CR creation when the org limit is exhausted

Testing

  • Unit tests added for pool satisfaction logic, pending deletion exclusion, zombie GC, and limit gate
  • Zombie GC tests: verifies deletion of zombies (Failed + no AWS ID + pool-owned) and preservation of real failures (Failed + has AWS ID)
  • Pool tests: NoState pending accounts satisfy pool, Failed accounts do not, mixed states work correctly, limit-reached blocks creation

Follow-up work

  1. Leaked account cleanup (separate PR): ~217 Failed accounts WITH AWS account IDs need investigation — these represent real AWS accounts that failed during setup but still exist in the organization
  2. Context threading (ROSAENG-62411): Thread ctx through helper functions to replace context.TODO() — the zombie GC code uses a //nolint:contextcheck for removeFinalizer which doesn't accept context yet
  3. Pool controller label selectors: Currently lists ALL Account CRs every reconcile with no label selector — should filter by pool ownership at the API level

BATMAN-JD and others added 2 commits July 30, 2026 10:46
Three interacting bugs caused hive-stage-01 to grow from 1232 to 1701
AWS accounts in one day:

1. Pool counts NoState zombies as satisfied — the satisfaction check
   `unclaimedAccounts >= poolSize` included stuck accounts that would
   never become Ready, inflating the count.

2. Burst creation overwhelms the account controller — when the pool
   was unsatisfied, it created all needed Account CRs in milliseconds.
   With MaxConcurrentReconciles=1, most timeout waiting for processing.

3. NoState zombies never get failed — accounts stuck on the AWS limit
   requeue forever without being marked Failed, permanently inflating
   pool counts and hiding the real shortfall.

Fix openshift#1: Change satisfaction check from `unclaimedAccounts >= poolSize`
to `available + progressing + pending >= poolSize`. Add
IsPendingFirstProcessing() to identify newly created Account CRs
(State="", non-failed, pool-owned, never-claimed). Each CR created by
the pool immediately counts as pending, naturally throttling creation
to exactly poolSize without needing an artificial delay.

Fix openshift#2: The pending counter IS the burst protection. When the pool
creates an Account CR, the ownership watch triggers re-reconcile. The
new CR counts as pending, so effectiveCount increases and the pool
only creates more if still below poolSize.

Fix openshift#3: Add zombie-failing logic in the account controller — fail
NoState accounts stuck on the AWS limit for longer than createPendTime
(25 min). Once failed, they drop out of the pending count and the pool
can create replacements (if the limit allows).

Additionally, add a pool-level account limit check: before creating
any Account CR, verify the AWS account count is below the limit. This
prevents the pool from creating CRs that would immediately become
zombies, eliminating the create-timeout-fail-replace churn cycle.

Co-Authored-By: Dakota Long <dlong@redhat.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 30, 2026
@openshift-ci

openshift-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@BATMAN-JD, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: c0518719-6757-4160-8d1f-43004ae63874

📥 Commits

Reviewing files that changed from the base of the PR and between 612989a and e1f335d.

⛔ Files ignored due to path filters (1)
  • api/v1alpha1/zz_generated.openapi.go is excluded by !**/zz_generated*
📒 Files selected for processing (7)
  • controllers/account/account_controller.go
  • controllers/account/account_controller_test.go
  • controllers/accountpool/accountpool_controller.go
  • controllers/accountpool/accountpool_controller_test.go
  • deploy/crds/aws.managed.openshift.io_accountpools.yaml
  • deploy_pko/CustomResourceDefinition-accountpools.aws.managed.openshift.io.yaml
  • pkg/totalaccountwatcher/totalaccountwatcher.go

Walkthrough

The change adds pending-account tracking to account pools, uses pending accounts in pool satisfaction, pauses creation at AWS limits, and cleans up failed pool-owned accounts without AWS IDs. It also records a pending condition for blocked accounts and updates generated CRD, pipeline, and ownership metadata.

Changes

Account capacity and lifecycle

Layer / File(s) Summary
Account pending and cleanup behavior
api/v1alpha1/account_types.go, controllers/account/account_controller.go, controllers/account/account_controller_test.go
Pending-deletion accounts are excluded from first processing; failed pool-owned accounts without AWS IDs are deleted; AWS-limit blocking sets a Pending condition and requeues after five minutes, with tests covering cleanup behavior.
Pool capacity calculation and enforcement
api/v1alpha1/accountpool_types.go, controllers/accountpool/*
Pool satisfaction uses available, progressing, and pending counts; accountsPending is exposed in status and the CRD; creation pauses when the AWS account limit is reached, with expanded reconciliation tests.
Generated and repository metadata
.tekton/*, OWNERS_ALIASES, deploy/crds/*
The boilerplate pipeline revision, ownership alias list, and CRD generator metadata are updated; the AccountPool CRD includes the pending-account schema and printer column.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AccountPoolReconciler
  participant AWSAccountWatcher
  participant KubernetesAPI
  AccountPoolReconciler->>AWSAccountWatcher: Read current account count and limit
  AccountPoolReconciler->>KubernetesAPI: List Account resources
  AccountPoolReconciler->>AccountPoolReconciler: Calculate effective count and pending status
  AccountPoolReconciler->>KubernetesAPI: Create Account resource if capacity remains
  AccountPoolReconciler-->>AccountPoolReconciler: Requeue for five minutes when limit is reached
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 15
✅ Passed checks (15 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed The changed Ginkgo titles are static strings; no dynamic/generated values or brittle interpolations were found.
Test Structure And Quality ✅ Passed New tests are single-purpose, use existing fake-client setup, and add no risky waits or leaked cluster resources.
Microshift Test Compatibility ✅ Passed Added Ginkgo tests are unit tests with fake clients/AWS mocks; they don't use MicroShift-unsupported OpenShift APIs, namespaces, or HA assumptions.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The new tests are controller unit tests with fake clients and no multi-node or SNO-specific assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed PASS: The PR only changes account reconciliation/status logic and CRDs; no pod scheduling, node selectors/affinity, topology spread, PDB, or replica-setting changes were added.
Ote Binary Stdout Contract ✅ Passed No PR-changed process-level init/main/suite code writes to stdout; controller changes use structured logging, and fmt.Printf calls are inside tests.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new e2e tests or network/IP assumptions were added; the new Ginkgo tests are unit tests using fake clients and no external connectivity.
No-Weak-Crypto ✅ Passed Changed files add account/pool logic only; no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB, crypto imports, or secret comparisons were found.
Container-Privileges ✅ Passed No changed K8s/Tekton manifests introduce privileged settings; the PipelineRun and CRDs contain no hostPID/hostNetwork/privileged/allowPrivilegeEscalation/root fields.
No-Sensitive-Data-In-Logs ✅ Passed New logs only emit an Account CR name (fixed prefix + UUID) and generic errors; no passwords, tokens, PII, or customer data found.
Title check ✅ Passed The title accurately summarizes the main changes: burst prevention, zombie cleanup, and limiting account churn.
Description check ✅ Passed The description covers the problem, changes, testing, and follow-up work, though it doesn't follow the repo template exactly.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 30, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/v1alpha1/account_types.go`:
- Around line 535-539: Update Account.IsPendingFirstProcessing to also require
that the account is not pending deletion by incorporating IsPendingDeletion into
the predicate, preserving the existing NoState, non-failed, never-claimed, and
pool-owned conditions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 997cbbd2-5a93-4a64-b13c-e9781dd500f6

📥 Commits

Reviewing files that changed from the base of the PR and between dabda29 and 62ceea9.

⛔ Files ignored due to path filters (4)
  • boilerplate/_data/last-boilerplate-commit is excluded by !boilerplate/**
  • boilerplate/openshift/golang-osd-operator/OWNERS_ALIASES is excluded by !boilerplate/**
  • build/Dockerfile is excluded by !build/**
  • build/Dockerfile.olm-registry is excluded by !build/**
📒 Files selected for processing (12)
  • .tekton/aws-account-operator-agentic-sdlc-check-pull-request.yaml
  • OWNERS_ALIASES
  • api/v1alpha1/account_types.go
  • api/v1alpha1/accountpool_types.go
  • controllers/account/account_controller.go
  • controllers/accountpool/accountpool_controller.go
  • controllers/accountpool/accountpool_controller_test.go
  • deploy/crds/aws.managed.openshift.io_accountclaims.yaml
  • deploy/crds/aws.managed.openshift.io_accountpools.yaml
  • deploy/crds/aws.managed.openshift.io_accounts.yaml
  • deploy/crds/aws.managed.openshift.io_awsfederatedaccountaccesses.yaml
  • deploy/crds/aws.managed.openshift.io_awsfederatedroles.yaml
💤 Files with no reviewable changes (1)
  • OWNERS_ALIASES

Comment thread api/v1alpha1/account_types.go
Replace the zombie-failing approach (marking accounts as Failed after
25 min) with an observable Condition (reason: AWSAccountLimitReached).

The key insight: accounts blocked by the AWS org limit are valid — they
just need capacity. Failing them destroys the recovery path and creates
unnecessary CR churn when the limit clears. Setting a Condition instead
keeps State empty so the account controller picks them up automatically
once AccountsCanBeCreated() returns true.

The Condition uses type=Pending with reason=AWSAccountLimitReached,
providing the same observability as a Failed state without the
permanence. LastProbeTime updates on each reconcile so operators can
see how long an account has been waiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@BATMAN-JD

Copy link
Copy Markdown
Contributor Author

/label tide/merge-method-squash

@openshift-ci openshift-ci Bot added the tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges. label Jul 30, 2026
Accounts with a DeletionTimestamp set should not count as pending
first processing — they're being torn down, not waiting for creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@BATMAN-JD BATMAN-JD changed the title fix: prevent pool burst creation and zombie churn cycle fix: prevent pool burst creation, zombie churn, and auto-cleanup Jul 30, 2026
Zombie Account CRs are Failed, pool-owned, and have no AWS account ID.
They accumulate when the pool controller creates accounts that hit the
AWS account limit before provisioning. These CRs are irrecoverable and
inflate the Account CR list, adding overhead to every pool reconcile.

On deploy, the operator will automatically strip finalizers and delete
all ~958 existing zombie CRs in staging, eliminating manual cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@BATMAN-JD
BATMAN-JD force-pushed the BATMAN-JD/fix-pool-satisfaction-and-burst branch from 612989a to 534aed9 Compare July 30, 2026 18:23
@BATMAN-JD
BATMAN-JD marked this pull request as ready for review July 30, 2026 18:33
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 30, 2026
@openshift-ci
openshift-ci Bot requested review from AlexSmithGH and geowa4 July 30, 2026 18:35
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.44444% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.76%. Comparing base (dabda29) to head (e1f335d).

Files with missing lines Patch % Lines
controllers/account/account_controller.go 13.79% 23 Missing and 2 partials ⚠️

❌ Your patch status has failed because the patch coverage (44.44%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1059      +/-   ##
==========================================
+ Coverage   46.60%   46.76%   +0.16%     
==========================================
  Files          46       46              
  Lines        7124     7163      +39     
==========================================
+ Hits         3320     3350      +30     
- Misses       3457     3464       +7     
- Partials      347      349       +2     
Files with missing lines Coverage Δ
api/v1alpha1/account_types.go 80.62% <100.00%> (+1.79%) ⬆️
api/v1alpha1/accountpool_types.go 100.00% <ø> (ø)
controllers/accountpool/accountpool_controller.go 64.60% <100.00%> (+3.43%) ⬆️
pkg/totalaccountwatcher/totalaccountwatcher.go 63.39% <ø> (ø)
controllers/account/account_controller.go 39.31% <13.79%> (-0.42%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

1. Add AccountsCanBeCreated() to AccountWatcherIface so the pool
   controller uses the same limit logic as the account controller.
   This fixes the startup deadlock where limit=0, count=0 evaluated
   as "limit reached" before the watcher's first poll, and inherits
   the fail-safe that defaults to false on AWS errors.

2. Switch limit-blocked Condition from write-every-reconcile to
   write-once. Check if the Pending condition with reason
   AWSAccountLimitReached already exists before writing, eliminating
   the status write storm with hundreds of blocked accounts.

3. Add test for the startup/watcher-uninitialized case (limit=0).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openshift-ci

openshift-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@BATMAN-JD: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

// These were created by the pool controller but never provisioned in AWS
// (e.g. account limit was reached). They are irrecoverable and inflate
// the Account CR list, adding overhead to every pool reconcile.
if !currentAcctInstance.HasAwsAccountID() && currentAcctInstance.IsOwnedByAccountPool() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd consider wrapping this in a feature flag to enable at will in staging and not impact our other environments. I'm not totally sure if in all environments if this set of conditionals on an account CR should always lead to use removing finalizers and deleting the account.

@AlexSmithGH AlexSmithGH left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/hold
/lgtm

@openshift-ci openshift-ci Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 30, 2026
@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 30, 2026
@openshift-ci

openshift-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: AlexSmithGH, BATMAN-JD

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [AlexSmithGH,BATMAN-JD]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@BATMAN-JD

Copy link
Copy Markdown
Contributor Author

/unhold

@openshift-ci openshift-ci Bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 31, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit 6342384 into openshift:master Jul 31, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged. tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants