Skip to content

fix(kubernetes): scale idle pool pods in when poolMax is lowered - #2470

Open
acodercat wants to merge 3 commits into
opensandbox-group:mainfrom
acodercat:fix/k8s-pool-scale-in-to-pool-max
Open

acodercat wants to merge 3 commits into
opensandbox-group:mainfrom
acodercat:fix/k8s-pool-scale-in-to-pool-max

Conversation

@acodercat

@acodercat acodercat commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

scalePool scales in only when the desired pool size, max(allocated + supply + desiredBuffer, poolMin), drops below the current number of schedulable pods. poolMax is applied only on the way up, as a limit on how many new pods to create (maxNewPods).

So when poolMax is lowered below what the buffer band or poolMin asks for, the pool stops growing but keeps every idle pod it already has. For example, with four ready idle pods and bufferMin: 1, bufferMax: 4, changing poolMax from 4 to 2 leaves all four pods running indefinitely. The CRD has no cross-field validation that would reject this, and docs/architecture/control-plane/operator.md describes poolMax as a bound on the whole pool.

Summary

  • Keep the existing scale-in to desiredSchedulableCnt, and also scale in the excess above poolMax. Pods that recycling or a rolling update already planned to delete count toward that excess, so the pool does not delete one of them plus an extra idle pod and then create one back.
  • pickPodsToDelete only picks idle pods, so allocated pods are never deleted to meet the bound. If the allocated pods alone exceed poolMax, the pool deletes its idle pods and then waits for sandboxes to be released.
  • The existing delete batch limit (scaleStrategy.maxUnavailable, at least one pod per batch) still paces the scale-in.
  • Scale-up is unchanged. I kept the cap out of desiredSchedulableCnt itself on purpose: that value also sizes the scale-up batch, and capping it would slow scale-up whenever demand exceeds poolMax. The desired-size scale-in is also unchanged, since it already accounts for rolling-update replacements through the supply count.

Testing

  • Not run (explain why)

  • Unit tests

  • Integration tests

  • e2e / manual verification

  • New table test, TestScalePoolScalesInDownToPoolMax. It covers three cases: a buffer band above a lowered poolMax (4 idle pods, 2 deleted), poolMin above poolMax (4 idle, 2 deleted), and allocated pods above poolMax (3 allocated and 1 idle, only the idle one deleted). Every case checks that only idle pods are deleted. All three fail on current main with no pod deleted.

  • TestScalePoolCountsPlannedDeletionsTowardPoolMax: five idle pods, poolMax: 4 and one planned deletion. It covers a recycled pod that is still listed as idle, and a rolling-update deletion with a replacement in supplyCnt. Only the planned pod is deleted.

  • The existing envtest specs that change poolMax, such as "should successfully scale out buffer limit by pool max" (poolMax: 2, bufferMin: 3, expects 2 pods), still hold with this change. I could not run the envtest suite locally (no etcd).

  • go test ./internal/controller/ -skip TestControllers: the only failures are TestReconcileDeletingTaskSandbox, TestReconcileDeletingPod and TestReconcilePodRecovery, which fail the same way on current main in my environment. go vet and golangci-lint (v2, repo config) are clean.

Breaking Changes

  • None
  • Yes (describe impact and migration path)

Pools whose poolMax is below their current idle pod count will now delete the extra idle pods, which is what poolMax is documented to do.

Checklist

  • Linked Issue or clearly described motivation
  • Added/updated docs (if needed)
  • Added/updated tests (if needed)
  • Security impact considered
  • Backward compatibility considered

scalePool only scaled in when the desired pool size,
max(allocated + supply + desired buffer, poolMin), dropped below the
current size. poolMax was applied only to scale-up, as a limit on new
pods. When poolMax is lowered below what the buffer band or poolMin
asks for, the pool stops growing but keeps every idle pod it already
has: with four ready idle pods and bufferMax 4, setting poolMax to 2
leaves four pods running indefinitely, although poolMax is documented
as the bound on the whole pool.

Scale in down to min(desired, poolMax). pickPodsToDelete only picks
idle pods, so allocated pods are never deleted to meet the bound, and
the existing delete batch limit still paces the scale-in. Scale-up is
unchanged.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T02:41:45.338023Z 0c5f8cb PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added component/k8s For kubernetes runtime size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Oct 29, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c5f8cbbc3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1208 to +1209
if target := min(desiredSchedulableCnt, pool.Spec.CapacitySpec.PoolMax); target < schedulableCnt {
scaleIn = schedulableCnt - target

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Count planned deletions toward the PoolMax reduction

When lowering poolMax coincides with a recycle or rolling-update entry in toDeletePods, this computes the full schedulableCnt - target even though that planned deletion is included in schedulableCnt. pickPodsToDelete appends all explicit deletions and then selects the full scaleIn again; with five idle pods, bufferMin: 4, poolMax: 4, one recycle deletion, and a delete budget of at least two, it deletes two pods, drops below the buffer minimum, and must create one back. Subtract already-selected deletions from the PoolMax excess before choosing additional idle pods.

AGENTS.md reference: kubernetes/AGENTS.md:L157-L157

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, thanks. Fixed in b4c42f9. The existing desired-size scale-in is back to what it was, because it already accounts for rolling-update replacements through the supply count. The PoolMax excess is added on top, measured after the planned deletions: max(schedulable - desired, schedulable - planned - poolMax). TestScalePoolCountsPlannedDeletionsTowardPoolMax covers your scenario with a recycled pod that is still listed as idle, and with a rolling-update deletion that has a replacement in supplyCnt. In both cases only the planned pod is deleted.

The scale-in target was min(desired, poolMax) applied to every pod in
the pool, but pods that recycling or a rolling update already planned
to delete are part of that count, and pickPodsToDelete deletes them in
addition to the idle pods chosen for scale-in. With five idle pods,
bufferMin 4, poolMax 4 and one planned deletion, the pool deleted two
pods, dropped below its buffer minimum and had to create one back.

Keep the existing desired-size scale-in unchanged, since it already
accounts for rolling-update replacements through the supply count, and
add the PoolMax excess separately, measured after the planned
deletions.
scalePool counted planned deletions by calling pickPodsToDelete with
no idle pods and no scale-in, which only works because of how that
function happens to be built. Move the selection of planned deletions
(pods that still exist, are not terminating, once each) into
plannedDeletions, and use it from both pickPodsToDelete and scalePool.
@github-actions github-actions Bot added size/S Denotes a PR that changes 10-29 lines, ignoring generated files. and removed size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Oct 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/k8s For kubernetes runtime size/S Denotes a PR that changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant