This is an automated email from the ASF dual-hosted git repository.
manirajv06 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-core.git
The following commit(s) were added to refs/heads/master by this push:
new d1772806 [YUNIKORN-3399] Align RollbackAllocation tests with the
ask-presence guard from YUNIKORN-3360 (#1136)
d1772806 is described below
commit d1772806f15bda6307ac1ac29580d76a747bbdd0
Author: Tigerquoll <[email protected]>
AuthorDate: Tue Aug 25 11:34:09 2026 +0530
[YUNIKORN-3399] Align RollbackAllocation tests with the ask-presence guard
from YUNIKORN-3360 (#1136)
YUNIKORN-3360 added a guard rejecting a RollbackAllocation whose ask is no
longer in sa.requests, while YUNIKORN-3352 added tests asserting the
previous
behaviour where such a "ghost" rollback succeeded; the two merged
independently
and conflict semantically, failing two tests on master. This aligns the
tests
with the guard's semantics, which also gives that guard its first direct
coverage: TestRollbackAllocationAskNotTracked now pins that a ghost
rollback is
rejected and leaves the allocation, the allocated resource and the pending
histogram untouched, and the property fuzzer counts ghost rollback attempts,
asserting each one is rejected and treating it as a no-op in the reference
model. Test-only change, no product code is touched.
Signed-off-by: Tigerquoll <[email protected]>
Closes: #1136
Signed-off-by: mani <[email protected]>
---
pkg/scheduler/objects/application_property_test.go | 86 ++++++++++++----------
pkg/scheduler/objects/application_test.go | 20 +++--
2 files changed, 60 insertions(+), 46 deletions(-)
diff --git a/pkg/scheduler/objects/application_property_test.go
b/pkg/scheduler/objects/application_property_test.go
index d07fe810..cb9b404c 100644
--- a/pkg/scheduler/objects/application_property_test.go
+++ b/pkg/scheduler/objects/application_property_test.go
@@ -43,26 +43,29 @@ import (
//
// The point is coverage of state combinations rather than of entry points:
TestMaxAskPriority pins
// the handful of transitions that are easy to reason about by hand, while
this test reaches the
-// interleavings that are not - a replaced ask whose priority differs from the
one it displaces, a
-// rollback of an allocation whose ask has already been dropped, an allocate
that empties the top
-// priority bucket while lower buckets still hold pending asks. Those are
precisely the cases where
-// incremental bookkeeping and a full rescan can disagree.
+// interleavings that are not - a replaced ask whose priority differs from the
one it displaces, an
+// attempted rollback of an allocation whose ask has already been dropped, an
allocate that empties
+// the top priority bucket while lower buckets still hold pending asks. Those
are precisely the cases
+// where incremental bookkeeping and a full rescan can disagree.
func TestApplicationPropertyFuzzHistogram(t *testing.T) {
- // A "ghost" rollback (case 7 reverting an allocation whose ask
sa.requests no longer holds) needs
- // a specific remove-then-release interleaving, so it is rare enough
that individual seeds
- // legitimately see none - measured, 6 of the 20 seeds below produce
zero. Assert that coverage
- // over the whole seed set rather than per seed: that still fails
loudly if a change stops the
- // fuzzer reaching the state at all, without making the test flaky on
the seeds that never do.
- totalGhostRollbacks := 0
+ // A "ghost" rollback attempt (case 7 picking a confirmed allocation
whose ask sa.requests no
+ // longer holds) first needs a specific remove-then-release
interleaving, but once one exists the
+ // YUNIKORN-3360 guard rejects the rollback and so leaves the entry in
sa.allocations for case 7 to
+ // pick again - measured, every one of the 20 seeds below reaches the
state a few hundred times.
+ // The coverage floor is still asserted over the whole seed set rather
than per seed so it keeps
+ // holding if that accumulation stops: it only has to fail loudly if a
change stops the fuzzer
+ // reaching the state at all. Whether each individual attempt is
correctly rejected is asserted in
+ // case 7 itself, not here.
+ totalGhostRollbackAttempts := 0
for seed := int64(0); seed < 20; seed++ {
t.Run(fmt.Sprintf("seed-%d", seed), func(t *testing.T) {
- totalGhostRollbacks += runPropertyFuzz(t, seed)
+ totalGhostRollbackAttempts += runPropertyFuzz(t, seed)
})
}
- assert.Assert(t, totalGhostRollbacks > 0, "no seed ever rolled back an
ask missing from sa.requests")
+ assert.Assert(t, totalGhostRollbackAttempts > 0, "no seed ever
attempted to roll back an ask missing from sa.requests")
}
-// runPropertyFuzz executes one seeded run and returns the number of ghost
rollbacks it performed, so
+// runPropertyFuzz executes one seeded run and returns the number of ghost
rollbacks it attempted, so
// the caller can assert that coverage across the whole seed set.
func runPropertyFuzz(t *testing.T, seed int64) int { //nolint:funlen
t.Helper()
@@ -119,7 +122,7 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
allocatedKeys := make(map[string]bool)
// confirmedKeys mirrors sa.allocations: keys handed to AddAllocation
and not yet removed from
// sa.allocations again (only a successful RollbackAllocation does that
here - application.go
- // ~line 886). Note that it is deliberately NOT wiped by the
clear-all/Failed cleanup branches in
+ // ~line 907). Note that it is deliberately NOT wiped by the
clear-all/Failed cleanup branches in
// case 5: removeAsksInternal and cleanupAsks only ever touch
sa.requests, so a stale entry does
// survive in sa.allocations there and the model must say so too.
confirmedKeys := make(map[string]bool)
@@ -132,12 +135,14 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
// that really ran deallocateAsk. A rejected call (wrong app state,
unknown key) mutates nothing,
// so counting attempts would let this operation decay into a no-op
unnoticed.
successfulRollbacks := 0
- // ghostRollbacks counts the subset of those that rolled back an ask
sa.requests no longer holds
- // (case 7); replacedAsks counts the AddAllocationAsk calls that took
the replace-existing-ask
- // branch (case 8). Both are narrow branches that a small change to the
candidate filters could
- // stop reaching entirely, so both are asserted - replacedAsks per run
at the end of this
- // function, ghostRollbacks over the whole seed set by the caller.
- ghostRollbacks := 0
+ // ghostRollbackAttempts counts the case 7 calls that targeted an ask
sa.requests no longer holds;
+ // those never succeed (the YUNIKORN-3360 guard rejects them) so
attempts, not successes, are what
+ // there is to count - each one is individually asserted rejected in
case 7, which is what makes
+ // the count proof that the guard was exercised. replacedAsks counts
the AddAllocationAsk calls
+ // that took the replace-existing-ask branch (case 8). Both are narrow
branches that a small change
+ // to the candidate filters could stop reaching entirely, so both are
asserted - replacedAsks per
+ // run at the end of this function, ghostRollbackAttempts over the
whole seed set by the caller.
+ ghostRollbackAttempts := 0
replacedAsks := 0
const steps = 5000
@@ -313,10 +318,10 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
// removeAsksInternal("") from
timeoutPlaceholderProcessing case 2, which is reached from
// Running for a soft gang whose ResumeApplication
transition is rejected (that event is
// only valid from New/Accepted, application_state.go
~line 125) so the app stays Running
- // and remains rollback-eligible. deallocateAsk must
not count such an ask as pending
- // again - nothing tracks it any more, so nothing would
ever take it back out of the
- // histogram - so the reference model below only marks
the key pending again when it is
- // still tracked.
+ // and remains rollback-eligible. Since YUNIKORN-3360,
RollbackAllocation rejects such a
+ // ghost before touching anything (application.go ~line
885: the ask has to be in
+ // sa.requests), so the fuzzer asserts that rejection
below, and the reference model
+ // leaves the step as the no-op it now is.
// The only combination excluded is a confirmed key
that is still tracked but already
// pending (deallocated without being removed):
ask.deallocate() rejects that outright, so
// picking it would only ever burn a step.
@@ -328,7 +333,15 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
_, tracked := keyPriority[k]
return !tracked
}); ok {
- if _, rollbackErr :=
app.RollbackAllocation(key); rollbackErr == nil {
+ _, tracked := keyPriority[key]
+ _, rollbackErr := app.RollbackAllocation(key)
+ if !tracked {
+ // ghost: sa.requests no longer holds
the ask. Since YUNIKORN-3360,
+ // RollbackAllocation must reject this
outright and mutate nothing.
+ assert.Assert(t, rollbackErr != nil,
"seed=%d step=%d: ghost rollback of %s was not rejected", seed, i, key)
+ ghostRollbackAttempts++
+ }
+ if rollbackErr == nil {
// only a successful call ran
deallocateAsk: RollbackAllocation refuses outright
// unless the app is Accepted or
Running (application.go ~line 864) and bails out
// before touching anything if the ask
is no longer allocated, so a returned error
@@ -336,15 +349,10 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
delete(confirmedKeys, key)
delete(allocatedKeys, key)
successfulRollbacks++
- if _, tracked := keyPriority[key];
tracked {
- // same as DeallocateAsk (case
2): the ask goes back to pending at its
- // immutable creation priority,
which keyPriority already holds.
- pendingKeys[key] = true
- } else {
- // ghost rollback: the ask is
gone from sa.requests, so deallocateAsk must
- // leave the pending histogram
completely untouched.
- ghostRollbacks++
- }
+ // a successful rollback implies the
ask was tracked (the 3360 guard rejects the
+ // rest), so - same as DeallocateAsk
(case 2) - it goes back to pending at its
+ // immutable creation priority, which
keyPriority already holds.
+ pendingKeys[key] = true
}
}
@@ -389,7 +397,7 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
// operation to be rejected/no-op'd): confirm it actually drove the
application through
// non-trivial pending/allocated/multi-priority states, so a real
regression in the product's
// bookkeeping (e.g. removeFromPriorities) has states to be caught in.
- t.Logf("seed=%d coverage: maxPending=%d maxAllocated=%d
maxDistinctPendingPriorities=%d successfulRollbacks=%d ghostRollbacks=%d
replacedAsks=%d", seed, maxPending, maxAllocated, maxDistinctPendingPriorities,
successfulRollbacks, ghostRollbacks, replacedAsks)
+ t.Logf("seed=%d coverage: maxPending=%d maxAllocated=%d
maxDistinctPendingPriorities=%d successfulRollbacks=%d ghostRollbackAttempts=%d
replacedAsks=%d", seed, maxPending, maxAllocated, maxDistinctPendingPriorities,
successfulRollbacks, ghostRollbackAttempts, replacedAsks)
assert.Assert(t, maxPending > 0, "seed=%d: fuzz run never observed any
pending asks", seed)
assert.Assert(t, maxAllocated > 0, "seed=%d: fuzz run never observed
any allocated asks", seed)
assert.Assert(t, maxDistinctPendingPriorities > 1, "seed=%d: fuzz run
never observed a multi-priority pending histogram", seed)
@@ -400,11 +408,13 @@ func runPropertyFuzz(t *testing.T, seed int64) int {
//nolint:funlen
assert.Assert(t, successfulRollbacks > 0, "seed=%d: fuzz run never
completed a RollbackAllocation", seed)
// The replace-existing-ask branch (case 8) must not double count the
key in the histogram. It is
// only reached while pendingKeys is non-empty, so assert it really
happened rather than trusting
- // that condition to keep holding. The ghost-rollback count is returned
instead of asserted here:
- // it is too rare to demand per seed, see
TestApplicationPropertyFuzzHistogram.
+ // that condition to keep holding. The ghost-rollback-attempt count is
returned instead of asserted
+ // here: whether a run reaches that state at all depends on the
interleaving it happens to produce,
+ // so the coverage floor for it is asserted over the whole seed set by
+ // TestApplicationPropertyFuzzHistogram.
assert.Assert(t, replacedAsks > 0, "seed=%d: fuzz run never took the
replace-existing-ask branch", seed)
- return ghostRollbacks
+ return ghostRollbackAttempts
}
// newFuzzAsk builds an Allocation directly from an si.Allocation (bypassing
the shared
diff --git a/pkg/scheduler/objects/application_test.go
b/pkg/scheduler/objects/application_test.go
index 27732e04..37e37123 100644
--- a/pkg/scheduler/objects/application_test.go
+++ b/pkg/scheduler/objects/application_test.go
@@ -2791,12 +2791,14 @@ func TestAddAllocationAskReplaceExistingPendingAsk(t
*testing.T) {
assertMaxPriorityConsistent(t, app)
}
-// TestRollbackAllocationAskNotTracked covers deallocateAsk running for an ask
that sa.requests no
-// longer holds. removeAsksInternal("") wipes sa.requests and the pending
histogram but deliberately
+// TestRollbackAllocationAskNotTracked covers RollbackAllocation being handed
a "ghost": an ask that
+// sa.requests no longer holds while sa.allocations still does. That state is
reachable in
+// production: removeAsksInternal("") wipes sa.requests and the pending
histogram but deliberately
// leaves sa.allocations alone until the shim confirms the releases, so a
SCHEDULING_FAILED_ON_RM
-// release arriving in that window reaches RollbackAllocation, which looks the
entry up in
-// sa.allocations and deallocates it. deallocateAsk must not count that ask as
pending again: the
-// application does not track it any more, so nothing would ever take it back
out of the histogram.
+// release arriving in that window reaches RollbackAllocation. Since
YUNIKORN-3360 the rollback is
+// rejected outright when the ask is not in sa.requests, and this test pins
that guard: the call must
+// return an error and leave the allocation, the allocated resource and the
pending histogram
+// untouched.
func TestRollbackAllocationAskNotTracked(t *testing.T) {
setupUGM()
defer setupUGM()
@@ -2820,12 +2822,14 @@ func TestRollbackAllocationAskNotTracked(t *testing.T) {
assert.Assert(t, app.IsAccepted() || app.IsRunning(), "app must still
be rollback-eligible, is %s", app.CurrentState())
_, err = app.RollbackAllocation(aKey)
- assert.NilError(t, err, "rollback of the confirmed allocation should
have succeeded")
+ assert.ErrorContains(t, err, "failed to locate ask", "rollback of an
untracked ask should have been rejected")
app.RLock()
- assert.Equal(t, len(app.pendingPriorities), 0, "rollback of an
untracked ask must not change the pending histogram")
+ assert.Assert(t, app.allocations[aKey] == ask, "rejected rollback must
leave the confirmed allocation in place")
+ assert.Equal(t, len(app.pendingPriorities), 0, "rejected rollback must
not change the pending histogram")
app.RUnlock()
- assert.Equal(t, app.GetAskMaxPriority(), configs.MinPriority, "rollback
of an untracked ask must not change askMaxPriority")
+ assert.Assert(t, resources.Equals(app.GetAllocatedResource(), res),
"rejected rollback must not change the allocated resource")
+ assert.Equal(t, app.GetAskMaxPriority(), configs.MinPriority, "rejected
rollback must not change askMaxPriority")
assertMaxPriorityConsistent(t, app)
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]