Skip to content

Commit 7ddb95c

Browse files
authored
Merge pull request #2036 from kai-scheduler/claude/upstream-resource-helpers-prototype
refactor(scheduler): delegate effective-request accounting to k8s.io/component-helpers
2 parents ee61814 + aacb74d commit 7ddb95c

6 files changed

Lines changed: 129 additions & 399 deletions

File tree

pkg/admission/webhook/v1alpha2/podhooks/pod_resize_validator.go

Lines changed: 22 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010

1111
corev1 "k8s.io/api/core/v1"
1212
schedulingv1 "k8s.io/api/scheduling/v1"
13-
"k8s.io/apimachinery/pkg/api/resource"
1413
"k8s.io/apimachinery/pkg/runtime"
14+
resourcehelpers "k8s.io/component-helpers/resource"
1515
"sigs.k8s.io/controller-runtime/pkg/client"
1616
logf "sigs.k8s.io/controller-runtime/pkg/log"
1717
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -24,15 +24,6 @@ import (
2424

2525
var resizeLog = logf.Log.WithName("pod-resize-validator")
2626

27-
func isPodResizeInfeasible(pod *corev1.Pod) bool {
28-
for _, c := range pod.Status.Conditions {
29-
if c.Type == corev1.PodResizePending {
30-
return c.Status == corev1.ConditionTrue && c.Reason == corev1.PodReasonInfeasible
31-
}
32-
}
33-
return false
34-
}
35-
3627
// memoryLimitBytesPerUnit converts a Queue Memory.Limit (in megabytes) to bytes.
3728
const memoryLimitBytesPerUnit = 1_000_000
3829

@@ -135,135 +126,40 @@ func (v *PodResizeValidator) validateResize(ctx context.Context, oldPod, newPod
135126
// podResizeDelta computes the net per-resource increase this resize introduces
136127
// relative to what the queue already accounts for.
137128
//
138-
// The queue's Status.Allocated is a pod-level sum of per-container effective
139-
// requests, so the delta is computed at the same granularity:
129+
// All three aggregates come from the same upstream helper the scheduler uses in
130+
// getPodResourceRequest, so the delta baseline is guaranteed to match queue
131+
// accounting by construction rather than by parallel hand-written logic:
140132
//
141-
// 1. For each regular container and each restartable init container (sidecar),
142-
// accumulate three pod-level sums: newSpecSum, oldSpecSum, effectiveOldSum.
143-
// 2. Skip a resource if its pod-level spec is unchanged (newSpecSum[r] ==
144-
// oldSpecSum[r]) — that resource was not part of this resize request and
145-
// must not generate spurious delta even when an earlier infeasible attempt
146-
// left an unresolved stale spec.
147-
// 3. For changed resources, delta = max(0, newSpecSum[r] - effectiveOldSum[r]).
148-
// Pod-level aggregation naturally handles CPU/memory moved between containers
149-
// (redistribution produces zero net delta at the pod level).
133+
// - newSpec : the resize target (spec only)
134+
// - oldSpec : the pre-resize target (spec only)
135+
// - effectiveOld : what the queue currently charges, i.e. the KEP-1287
136+
// effective request max(spec, enacted, allocated), or max(enacted, allocated)
137+
// when the kubelet marked the resize Infeasible
150138
//
151-
// Effective old baseline per container:
152-
// - If old pod has a current Infeasible condition: max(enacted, allocated).
153-
// The infeasible spec was never committed by the kubelet, so the queue only
154-
// reflects enacted/allocated.
155-
// - Otherwise (normal / Deferred / InProgress): max(spec, enacted, allocated),
156-
// matching what the scheduler charges for those states.
157-
// - Falls back to old spec when no ContainerStatus is available.
139+
// A resource whose spec is unchanged is skipped: it is not part of this resize,
140+
// and an unresolved Infeasible spec on it must not produce phantom delta.
158141
func podResizeDelta(oldPod, newPod *corev1.Pod) corev1.ResourceList {
159-
oldInfeasible := isPodResizeInfeasible(oldPod)
160-
161-
statusByName := make(map[string]*corev1.ContainerStatus, len(oldPod.Status.ContainerStatuses))
162-
for i := range oldPod.Status.ContainerStatuses {
163-
statusByName[oldPod.Status.ContainerStatuses[i].Name] = &oldPod.Status.ContainerStatuses[i]
164-
}
165-
oldByName := make(map[string]*corev1.Container, len(oldPod.Spec.Containers))
166-
for i := range oldPod.Spec.Containers {
167-
oldByName[oldPod.Spec.Containers[i].Name] = &oldPod.Spec.Containers[i]
168-
}
169-
170-
initStatusByName := make(map[string]*corev1.ContainerStatus, len(oldPod.Status.InitContainerStatuses))
171-
for i := range oldPod.Status.InitContainerStatuses {
172-
initStatusByName[oldPod.Status.InitContainerStatuses[i].Name] = &oldPod.Status.InitContainerStatuses[i]
173-
}
174-
oldInitByName := make(map[string]*corev1.Container, len(oldPod.Spec.InitContainers))
175-
for i := range oldPod.Spec.InitContainers {
176-
oldInitByName[oldPod.Spec.InitContainers[i].Name] = &oldPod.Spec.InitContainers[i]
177-
}
142+
specOnly := resourcehelpers.PodResourcesOptions{}
143+
withStatus := resourcehelpers.PodResourcesOptions{UseStatusResources: true}
178144

179-
newSpecSum := corev1.ResourceList{}
180-
oldSpecSum := corev1.ResourceList{}
181-
effectiveOldSum := corev1.ResourceList{}
145+
newSpec := resourcehelpers.AggregateContainerRequests(newPod, specOnly)
146+
oldSpec := resourcehelpers.AggregateContainerRequests(oldPod, specOnly)
147+
effectiveOld := resourcehelpers.AggregateContainerRequests(oldPod, withStatus)
182148

183-
for i := range newPod.Spec.Containers {
184-
accumulateDeltaSums(&newPod.Spec.Containers[i], oldByName, statusByName, oldInfeasible, newSpecSum, oldSpecSum, effectiveOldSum)
185-
}
186-
for i := range newPod.Spec.InitContainers {
187-
c := &newPod.Spec.InitContainers[i]
188-
if c.RestartPolicy == nil || *c.RestartPolicy != corev1.ContainerRestartPolicyAlways {
189-
continue
190-
}
191-
accumulateDeltaSums(c, oldInitByName, initStatusByName, oldInfeasible, newSpecSum, oldSpecSum, effectiveOldSum)
192-
}
193-
194-
zero := resource.MustParse("0")
195149
delta := corev1.ResourceList{}
196-
for r, newQty := range newSpecSum {
197-
if oldQty, ok := oldSpecSum[r]; ok && newQty.Cmp(oldQty) == 0 {
198-
continue // resource not changed by this resize at the pod level
150+
for resName, newQty := range newSpec {
151+
if oldQty, ok := oldSpec[resName]; ok && newQty.Cmp(oldQty) == 0 {
152+
continue // resource not changed by this resize
199153
}
200-
effectiveOld := effectiveOldSum[r]
201154
diff := newQty.DeepCopy()
202-
diff.Sub(effectiveOld)
203-
if diff.Cmp(zero) > 0 {
204-
delta[r] = diff
155+
diff.Sub(effectiveOld[resName])
156+
if diff.Sign() > 0 {
157+
delta[resName] = diff
205158
}
206159
}
207160
return delta
208161
}
209162

210-
// accumulateDeltaSums accumulates one container's contribution into three pod-level
211-
// sums. The effective old baseline follows the queue's own accounting:
212-
// - infeasible old pod: max(enacted, allocated)
213-
// - otherwise: max(old_spec, enacted, allocated)
214-
func accumulateDeltaSums(
215-
newC *corev1.Container,
216-
oldByName map[string]*corev1.Container,
217-
statusByName map[string]*corev1.ContainerStatus,
218-
oldInfeasible bool,
219-
newSpecSum, oldSpecSum, effectiveOldSum corev1.ResourceList,
220-
) {
221-
oldC := oldByName[newC.Name]
222-
cs := statusByName[newC.Name]
223-
224-
for resName, newQty := range newC.Resources.Requests {
225-
cur := newSpecSum[resName]
226-
cur.Add(newQty)
227-
newSpecSum[resName] = cur
228-
229-
var oldSpecQty resource.Quantity
230-
if oldC != nil {
231-
oldSpecQty = oldC.Resources.Requests[resName]
232-
}
233-
cur = oldSpecSum[resName]
234-
cur.Add(oldSpecQty)
235-
oldSpecSum[resName] = cur
236-
237-
effectiveOld := oldSpecQty
238-
if cs != nil {
239-
candidates := []resource.Quantity{}
240-
if !oldInfeasible {
241-
candidates = append(candidates, oldSpecQty)
242-
}
243-
if cs.Resources != nil {
244-
if enacted, ok := cs.Resources.Requests[resName]; ok {
245-
candidates = append(candidates, enacted)
246-
}
247-
}
248-
if alloc, ok := cs.AllocatedResources[resName]; ok {
249-
candidates = append(candidates, alloc)
250-
}
251-
if len(candidates) > 0 {
252-
best := candidates[0]
253-
for _, q := range candidates[1:] {
254-
if q.Cmp(best) > 0 {
255-
best = q
256-
}
257-
}
258-
effectiveOld = best
259-
}
260-
}
261-
cur = effectiveOldSum[resName]
262-
cur.Add(effectiveOld)
263-
effectiveOldSum[resName] = cur
264-
}
265-
}
266-
267163
func checkQueueCapacity(
268164
queue *v2.Queue,
269165
delta corev1.ResourceList,

pkg/admission/webhook/v1alpha2/podhooks/pod_resize_validator_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,3 +519,37 @@ func TestPodResizeValidator_Redistribution_AllowedAtLimit(t *testing.T) {
519519
resp := v.Handle(context.Background(), makeRequest(t, oldPod, newPod))
520520
assert.True(t, resp.Allowed, "CPU redistribution with unchanged pod total should be allowed even at limit")
521521
}
522+
523+
// TestPodResizeValidator_InitPeakDominates_NoDelta covers a pod whose queue charge is
524+
// dominated by the init-phase peak rather than the steady-state sum. Resizing a regular
525+
// container below that peak does not change what the queue charges, so the delta must be
526+
// zero even though the container itself grew.
527+
func TestPodResizeValidator_InitPeakDominates_NoDelta(t *testing.T) {
528+
scheme := buildScheme()
529+
// Queue is exactly at its limit: 10 CPU limit, 10 CPU allocated (the init peak).
530+
queue := newQueue("q", 10000, -1, 0, "10", "0")
531+
pg := newPodGroup("pg", "ns", "q")
532+
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(queue, pg).Build()
533+
v := NewPodResizeValidator(c, scheme, testSchedulerName, true, false)
534+
535+
makePod := func(mainCPU string) *corev1.Pod {
536+
p := podWithRequests("ns", "p", "pg", testSchedulerName, mainCPU, "0")
537+
p.Spec.InitContainers = []corev1.Container{
538+
{
539+
Name: "heavy-init",
540+
Resources: corev1.ResourceRequirements{
541+
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("10")},
542+
},
543+
},
544+
}
545+
return p
546+
}
547+
// Pod charge = max(steady, initPeak) = max(1,10) = 10 before, max(2,10) = 10 after.
548+
oldPod := makePod("1")
549+
newPod := makePod("2")
550+
551+
assert.Empty(t, podResizeDelta(oldPod, newPod), "init-peak-dominated resize should produce no delta")
552+
553+
resp := v.Handle(context.Background(), makeRequest(t, oldPod, newPod))
554+
assert.True(t, resp.Allowed, "queue charge is unchanged, so the resize must be admitted at the limit")
555+
}

pkg/scheduler/api/pod_info/effective_requests.go

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)