Skip to content

Commit e581c0d

Browse files
committed
Bind PreemptionNoCandidates exclusion to effective AFS ordering
The exclusion only checked AdmissionScope.AdmissionMode, but what actually reorders the queue is enableAdmissionFs, which needs admissionFairSharing configured and the feature gate on. Switched to testing enableAdmissionFs directly since that's the same value the heap comparator uses, so the exclusion can't drift out of sync with actual pop order. Also dropped the now-unused AdmissionScope assignment in Update(). TestRequeueHashTriggerByReason now passes a real config and enables the gate; its UBAFS cases previously passed only because of this gap. This in turn replaces the earlier assumption that AdmissionMode alone was enough to gate this. See Kueue#14231
1 parent c0b6bd4 commit e581c0d

2 files changed

Lines changed: 40 additions & 24 deletions

File tree

pkg/cache/queue/cluster_queue.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ type ClusterQueue struct {
193193

194194
AdmissionScope *kueue.AdmissionScope
195195

196+
// enableAdmissionFs reports whether this ClusterQueue actually pops pending
197+
// workloads by LocalQueue usage rather than by the queue-order timestamp.
198+
// Set once at construction from afs.ResourceWeights, the same value the heap
199+
// comparator captured, so the two can never disagree. Immutable afterwards,
200+
// so it is read without holding rwm.
201+
enableAdmissionFs bool
202+
196203
afsUsageLedger *queueafs.AfsUsageLedger
197204

198205
// lqWeights holds the LocalQueues that belong to this ClusterQueue, mapped to
@@ -315,6 +322,7 @@ func newClusterQueueImpl(ctx context.Context, client client.Client, cl *metrics.
315322
snapshotSort: snapshotSort,
316323
rwm: sync.RWMutex{},
317324
clock: clock,
325+
enableAdmissionFs: options.enableAdmissionFs,
318326
afsUsageLedger: options.afsUsageLedger,
319327
lqWeights: lqWeights,
320328
pw: &pw,
@@ -333,7 +341,6 @@ func (c *ClusterQueue) Update(apiCQ *kueue.ClusterQueue) error {
333341
}
334342
c.namespaceSelector = nsSelector
335343
c.active = apimeta.IsStatusConditionTrue(apiCQ.Status.Conditions, kueue.ClusterQueueActive)
336-
c.AdmissionScope = apiCQ.Spec.AdmissionScope
337344
if features.Enabled(features.ConcurrentAdmission) {
338345
c.ConcurrentAdmissionPolicy = apiCQ.Spec.ConcurrentAdmissionPolicy
339346
}
@@ -590,8 +597,14 @@ func (c *ClusterQueue) requeueIfNotPresent(log logr.Logger, wInfo *workload.Info
590597
}
591598
log.V(2).Info(logMsg, "clusterQueue", c.name, "workload", key)
592599

600+
// PreemptionNoCandidates is excluded when this queue pops by LocalQueue usage:
601+
// the scheduling-equivalence shape does not encode the queue-order timestamp,
602+
// which under a LowerOrNewerEqualPriority preemption policy decides whether a
603+
// workload may preempt an equal-priority victim. Two workloads can therefore
604+
// share a hash but not a preemption verdict, so a class-wide conclusion would
605+
// defer workloads that are in fact eligible. See Kueue#14231.
593606
if features.Enabled(features.SchedulingEquivalenceHashing) && wInfo.SchedulingHash != workload.SchedulingHashUnknown &&
594-
(reason == RequeueReasonNoFit || (reason == RequeueReasonPreemptionNoCandidates && !c.usesUsageBasedAdmissionFairSharing())) {
607+
(reason == RequeueReasonNoFit || (reason == RequeueReasonPreemptionNoCandidates && !c.enableAdmissionFs)) {
595608
if moved := c.handleInadmissibleHash(wInfo.SchedulingHash, resolveQuotaReservedReason(quotaReservedReason)); moved > 0 {
596609
log.V(2).Info("Bulk-moved equivalent workloads to inadmissible", "hash", wInfo.SchedulingHash, "movedCount", moved)
597610
}
@@ -600,16 +613,6 @@ func (c *ClusterQueue) requeueIfNotPresent(log logr.Logger, wInfo *workload.Info
600613
return true
601614
}
602615

603-
// usesUsageBasedAdmissionFairSharing reports whether this ClusterQueue pops
604-
// pending workloads ordered by LocalQueue usage instead of the queue-order
605-
// timestamp. The scheduling-equivalence shape does not encode that timestamp,
606-
// so a class-wide PreemptionNoCandidates conclusion reached under this mode
607-
// can incorrectly defer an equivalent workload that is actually eligible to
608-
// preempt under a LowerOrNewerEqualPriority preemption policy. See Kueue#14231.
609-
func (c *ClusterQueue) usesUsageBasedAdmissionFairSharing() bool {
610-
return c.AdmissionScope != nil && c.AdmissionScope.AdmissionMode == kueue.UsageBasedAdmissionFairSharing
611-
}
612-
613616
// handleInadmissibleHash bulk-moves all heap workloads matching the given
614617
// scheduling hash to inadmissibleWorkloads. Returns the number moved.
615618
// Only applies to BestEffortFIFO queues; in StrictFIFO the head workload

pkg/cache/queue/cluster_queue_test.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,9 +2041,14 @@ func TestQueueInadmissibleWorkloadsClearsHashes(t *testing.T) {
20412041
}
20422042

20432043
func TestRequeueHashTriggerByReason(t *testing.T) {
2044+
usageBasedAFS := &kueue.AdmissionScope{AdmissionMode: kueue.UsageBasedAdmissionFairSharing}
2045+
20442046
tests := map[string]struct {
2045-
reason RequeueReason
2047+
reason RequeueReason
2048+
// admissionScope and afsConfig must both be set for the ClusterQueue to
2049+
// actually order by LocalQueue usage; see afs.ResourceWeights.
20462050
admissionScope *kueue.AdmissionScope
2051+
afsConfig *config.AdmissionFairSharing
20472052
wantHash bool
20482053
}{
20492054
"nofit triggers hash": {
@@ -2067,28 +2072,36 @@ func TestRequeueHashTriggerByReason(t *testing.T) {
20672072
wantHash: false,
20682073
},
20692074
"nofit triggers hash under usage-based admission fair sharing": {
2070-
reason: RequeueReasonNoFit,
2071-
admissionScope: &kueue.AdmissionScope{
2072-
AdmissionMode: kueue.UsageBasedAdmissionFairSharing,
2073-
},
2074-
wantHash: true,
2075+
reason: RequeueReasonNoFit,
2076+
admissionScope: usageBasedAFS,
2077+
afsConfig: &config.AdmissionFairSharing{},
2078+
wantHash: true,
20752079
},
20762080
"preempt no candidates does not trigger hash under usage-based admission fair sharing": {
20772081
// The scheduling shape omits the queue-order timestamp that
20782082
// UsageBasedAdmissionFairSharing's LocalQueue-usage-first pop order can
20792083
// reorder relative to LowerOrNewerEqualPriority preemption eligibility, so a
20802084
// class-wide PreemptionNoCandidates conclusion is unsafe here. See Kueue#14231.
2081-
reason: RequeueReasonPreemptionNoCandidates,
2082-
admissionScope: &kueue.AdmissionScope{
2083-
AdmissionMode: kueue.UsageBasedAdmissionFairSharing,
2084-
},
2085-
wantHash: false,
2085+
reason: RequeueReasonPreemptionNoCandidates,
2086+
admissionScope: usageBasedAFS,
2087+
afsConfig: &config.AdmissionFairSharing{},
2088+
wantHash: false,
2089+
},
2090+
"preempt no candidates triggers hash when admission mode is set but admission fair sharing is not configured": {
2091+
// Without admissionFairSharing in the Configuration the queue still pops
2092+
// oldest-first, so the queue-order timestamp gap does not apply and the
2093+
// optimization must be retained.
2094+
reason: RequeueReasonPreemptionNoCandidates,
2095+
admissionScope: usageBasedAFS,
2096+
afsConfig: nil,
2097+
wantHash: true,
20862098
},
20872099
}
20882100

20892101
for name, tc := range tests {
20902102
t.Run(name, func(t *testing.T) {
20912103
features.SetFeatureGateDuringTest(t, features.SchedulingEquivalenceHashing, true)
2104+
features.SetFeatureGateDuringTest(t, features.AdmissionFairSharing, true)
20922105
ctx, _ := utiltesting.ContextWithLog(t)
20932106
cq, _ := newClusterQueue(ctx, nil,
20942107
&kueue.ClusterQueue{
@@ -2098,7 +2111,7 @@ func TestRequeueHashTriggerByReason(t *testing.T) {
20982111
},
20992112
}, nil,
21002113
workload.Ordering{PodsReadyRequeuingTimestamp: config.EvictionTimestamp},
2101-
nil, nil)
2114+
tc.afsConfig, nil)
21022115

21032116
wl := utiltestingapi.MakeWorkload("workload-1", defaultNamespace).
21042117
Request(corev1.ResourceCPU, "1").Obj()

0 commit comments

Comments
 (0)