feat(rbgsa-controller): rollout-aware scale-down and spec.replicas drift detection#270
feat(rbgsa-controller): rollout-aware scale-down and spec.replicas drift detection#270sebest wants to merge 7 commits into
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
160a7f2 to
66ab30f
Compare
|
@sebest I think the motivation behind this change makes a lot of sense. Please give us a little time to think through the API and workflow design, and I’ll follow up with feedback afterward. |
ee6febd to
820664c
Compare
|
@sebest We reviewed this PR in our community weekly meeting, and overall everyone really appreciated both the motivation and the design. The only feedback we had was around the naming/semantics of |
|
@sebest the e2e failure is not caused by this PR. It was due to an issue on the main branch, which I’ve already fixed. Could you please rebase onto the latest main branch and push again to rerun the e2e tests? Thanks! |
|
@sebest btw, We’d also be glad to have you join the community discussions. If you’re interested, please feel free to reach out to me at tongyuguo@outlook.com. |
StatefulSet/LWS delete pods from the highest ordinals on scale-down, which are the already-updated pods (ordinal >= partition). This destroys rollout progress and forces the rollout to restart from zero. Gate scale-down at the RBGSA controller level for partition-based workloads (StatefulSet, LWS, RoleInstanceSet) while a rolling update is in progress. Scale-up and Deployment-backed roles are unaffected. Changes: - Add Conditions field and ScaleDownDeferred condition to RBGSA status - Add rollout detection via RoleStatus.UpdatedReplicas < Replicas - Add Watches() on RBG for event-driven rollout completion detection - Emit Warning event on first deferral only (rate-limited) - Use SSA for all status writes (consistent field manager ownership) - DeepCopy after Get to prevent informer cache corruption Closes: sgl-project#269
Add ScaleDownPolicy field to ScalingAdapter spec with two values: - DeferDuringRollout (default): defers scale-down during partition-based rollout to prevent rollout progress destruction - Unrestricted: allows scale-down at any time (opt-out) The default is DeferDuringRollout when the field is unset, making the gating behavior the safe default without requiring explicit opt-in.
Change default ScaleDownPolicy from DeferDuringRollout to Unrestricted for backward compatibility — existing deployments are unaffected unless users explicitly opt in with scaleDownPolicy: DeferDuringRollout. Documentation: - Expand doc/features/autoscaler.md with full scaling adapter guide, ScaleDownPolicy explanation, and the partition ordinal problem - Add scaleDownPolicy field to doc/reference/api.md - Add ScaleDownDeferred condition to doc/reference/variables.md - Add autoscaling example link to doc/TOC.md
Extract deferScaleDownIfNeeded() and scaleRole() from the Reconcile method to reduce cyclomatic complexity from 31 to under 30. Also fix gofmt import ordering.
The kubebuilder binary is not used by `make test` (which runs plain `go test`). The v2.3.1 download fails on arm64 runners since that release predates arm64 support, causing spurious CI failures. Envtest tests use `setup-envtest` (installed via Makefile) instead.
Add 6 envtest tests covering the full controller interaction: StatefulSet with DeferDuringRollout policy: - Scale-down deferred during rollout (condition set, replicas unchanged) - Scale-down proceeds after rollout completes (via RBG status watch) - Scale-up during rollout proceeds normally StatefulSet with Unrestricted policy: - Scale-down during rollout proceeds (no gating) Deployment with DeferDuringRollout policy: - Scale-down during rollout proceeds (Deployments are not partition-based) Existing basic_test.go test continues to pass. In envtest, STS status is manually patched to simulate rollout state since no real StatefulSet controller runs. For the "rollout completes" test, RBG status is patched directly because the RBG controller cannot maintain accurate RoleStatuses without a real STS controller to track ObservedGeneration.
820664c to
9e754d4
Compare
Extend RBGRoleSpecOrStatusPredicate (renamed from RBGRoleStatusPredicate) to also fire when any role's spec.replicas value changes between the old and new RBG. Without this, the RBGSA controller's writeback path only wakes on status changes, so an external mutator that stomps spec.roles[].replicas (e.g. helm upgrade dropping the field, kubectl edit, non-SSA scripts) waits for the next status-change event before the apiserver-stored spec is corrected. Pairs with the in-memory override added in the RBG controller (PR sgl-project#306): the override prevents the workload from being sized to the stale value on the next reconcile, and this predicate change closes the latency gap by waking the writeback immediately on the stomp instead of when status next changes. The new rolesReplicasDiffer helper matches roles by name; added, removed, and renamed roles all count as a difference. We deliberately do not fire on every spec change — other fields (image, env, command) churn frequently and the adapter reconcile is irrelevant to them.
| } | ||
|
|
||
| logger := log.FromContext(ctx) | ||
| msg := fmt.Sprintf( |
There was a problem hiding this comment.
Structured logger (klog/logr) doesn't interpret %s format verbs — this ends up logging the literal %s in the message string. Should be key-value style:
logger.Error(err, "Failed to update status", "rbgScalingAdapterName", rbgScalingAdapter.Name)
cheyang
left a comment
There was a problem hiding this comment.
Review: rollout-aware scale-down & spec.replicas drift detection
Status: This PR currently has merge conflicts — needs rebase onto latest main (as previously requested).
Architecture & Design
The approach is well-considered:
- Gating at the RBGSA controller level rather than the workload controller is the right call — it keeps the scaling adapter self-contained and avoids entangling rollout logic with the core RBG reconciler.
- The distinction between partition-based workloads (STS, LWS, RoleInstanceSet) and Deployments is correctly scoped via
IsStatefulRole(). - Default of
Unrestrictedpreserves backward compatibility without surprise behavioral changes for existing users. - The
rolesReplicasDifferpredicate for spec.replicas drift detection is narrowly scoped (only fires on replica count changes, not other spec mutations), which avoids excessive reconcile churn.
Correctness
deferScaleDownIfNeededcorrectly short-circuits for scale-up (desiredReplicas >= currentReplicas).- Rollout detection via
UpdatedReplicas < Replicasfrom RBG status is reasonable — it reflects the workload controller's view of partition progress. - Event emission only on first deferral (checking condition presence before emitting) avoids log/event noise on every 30s requeue.
- The DeepCopy after Get prevents informer cache mutation — important since conditions are modified in-place.
- SSA for status writes is consistent with the existing pattern in this controller.
Minor Issue
One structured-logger misuse (see inline comment): logger.Error(err, "Failed to update status for %s", ...) — %s isn't interpolated by logr.
Tests
Coverage is thorough:
- 13 table-driven unit test cases covering all permutations of workload type, policy, rollout state, and scaling direction
- Condition lifecycle tests (set/clear/idempotent re-set)
- 6 envtest integration tests covering the full controller interaction including scale subresource writes and RBG status watch
- Predicate tests for the new
rolesReplicasDifferlogic
Summary
The design and implementation are solid. The blocking issue is the merge conflict. Once rebased and CI passes, this looks ready to merge pending the one-line logger fix.
|
Nicely designed feature with strong test coverage (810 lines of unit tests + 350 lines of envtest). The rollout-gating logic, condition lifecycle, and SSA status writes all look correct. The DeepCopy-after-Get fix for informer cache safety is a good catch. I left one concrete comment on a malformed The PR currently has merge conflicts with |
| rbgScalingAdapterApplyConfig := ToRoleBasedGroupScalingAdapterApplyConfiguration(rbgScalingAdapter). | ||
| WithStatus( | ||
| ToRoleBasedGroupScalingAdapterStatusApplyConfiguration(rbgScalingAdapter.Status, true).WithReplicas(*desiredReplicas), | ||
| ToRoleBasedGroupScalingAdapterStatusApplyConfiguration(rbgScalingAdapter.Status, true).WithReplicas(desiredReplicas), |
There was a problem hiding this comment.
This logger.Error call uses %s in the message string and passes rbgScalingAdapter.Name as a lone positional arg:
logger.Error(err, "Failed to update status for %s", rbgScalingAdapter.Name)In logr, Error(err, msg, keysAndValues...) does not interpolate format verbs -- %s appears literally in the output, and the adapter name becomes an orphaned key (odd-numbered arg) that triggers a MISSING value warning in the structured log.
Consider:
logger.Error(err, "Failed to update status", "rbgScalingAdapterName", rbgScalingAdapter.Name)| {Name: "worker", Replicas: 10, ReadyReplicas: 7, UpdatedReplicas: 3}, | ||
| }, | ||
| expectDeferred: true, | ||
| expectRequeueAfter: 30 * time.Second, |
There was a problem hiding this comment.
The condition lifecycle tests cover deferral, rollout completion, and HPA adjusting upward, but there is no test for the scenario where the HPA adjusts desired replicas to exactly match current replicas while a ScaleDownDeferred condition is active. In that case clearScaleDownDeferredCondition on the "nothing to do" path should remove the stale condition. A small table-driven case would close this gap.
This is a minor gap -- the path is exercised implicitly, but an explicit test documents the expected behavior.
|
Hi @sebest — this PR now conflicts with |
This PR adds two related RBGSA-controller behaviors. They are bundled because the second reuses the watch infrastructure introduced for the first.
Summary
1. Rollout-aware scale-down gating (original scope)
scalingAdapter.scaleDownPolicy: DeferDuringRollout(default isUnrestrictedfor backward compatibility).2. Wake on per-role
spec.replicasdrift (new — second commit)RBGRoleStatusPredicate→RBGRoleSpecOrStatusPredicate) to also fire when any role'sspec.replicaschanges between old and new.spec.roles[].replicas(e.g.helm upgradewhose template stops emitting the field,kubectl editfrom a manifest that omits replicas, non-SSA scripts) waits for the next status-change event before the apiserver-stored spec is corrected.rolesReplicasDifferhelper matches roles by name; added, removed, and renamed roles all count as a difference. Other spec fields (image, env, command) deliberately do not fire the predicate — the adapter reconcile is irrelevant to them.Changes
Rollout-aware scale-down:
Conditionsfield andScaleDownDeferredcondition to RBGSA statusScaleDownPolicyfield toScalingAdapterspec (DeferDuringRollout/Unrestricted)RoleStatus.UpdatedReplicas < Replicas(workload-agnostic)IsStatefulRole()helperWatches()on RBG for event-driven rollout completion detection (resolves existing TODO)doc/features/autoscaler.mdwith full scaling adapter documentationSpec drift detection:
RBGRoleStatusPredicate→RBGRoleSpecOrStatusPredicate; restructureUpdateFuncto OR-in a per-rolespec.replicascheckrolesReplicasDifferhelper (matches by role name; nil/non-nil pointer transitions and added/removed/renamed roles all count as differences)Design proposal
Closes #269
Companion PR
applyRBGSAReplicasOverrideat the top of the RBGReconcileso the workload is never sized from a stale spec value. Together with the predicate extension here, this provides defense-in-depth against external mutators that dropspec.roles[].replicas.Test plan
TestScaleDownDeferredDuringRollout,TestScaleDownDeferredConditionLifecycle,TestFindScalingAdaptersByRBG(rollout gating)TestRBGRoleSpecOrStatusPredicate(renamed; extended with 6 new sub-tests covering spec.replicas nil↔set, equal-but-other-fields-differ, added/removed/renamed roles)TestRBGStatusChangedPredicate(envtest-side, also updated to use the renamed predicate)go test ./internal/controller/workloads/...)make generate && make manifests— codegen cleanmake lint— passes (cyclomatic complexity under 30)helm upgradethat dropsspec.roles[].replicas, observe RBGSA writeback fires immediately (paired with fix(rbg-controller): override role.Replicas from bound RBGSA #306)