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 784bdc46 [YUNIKORN-3325] Deduplicate cancel reservations code flow 
(#1116)
784bdc46 is described below

commit 784bdc46d96ad7700cf33d359e7679c1f403c7d1
Author: kaijaytu <[email protected]>
AuthorDate: Tue Aug 4 11:53:20 2026 +0530

    [YUNIKORN-3325] Deduplicate cancel reservations code flow (#1116)
    
    Extract unreserveForApp() and cancelMatchingReservations() from the
    duplicated reservation cancellation logic in application.go and
    preemption.go.
    
    Both cancelReservations (tryRequiredNode path) and initWorkingState
    (preemption path) shared the same lock-aware unreserve pattern that
    branches on whether the reservation belongs to the calling app.
    Consolidate into a single predicate-based method that both call sites
    now use with their own filter logic.
    
    Closes: #1116
    
    Signed-off-by: mani <[email protected]>
---
 pkg/scheduler/objects/application.go      | 60 ++++++++++++-------
 pkg/scheduler/objects/application_test.go | 98 +++++++++++++++++++++++++++++++
 pkg/scheduler/objects/preemption.go       | 39 ++++--------
 3 files changed, 146 insertions(+), 51 deletions(-)

diff --git a/pkg/scheduler/objects/application.go 
b/pkg/scheduler/objects/application.go
index d5b408cf..a125e0ac 100644
--- a/pkg/scheduler/objects/application.go
+++ b/pkg/scheduler/objects/application.go
@@ -1227,33 +1227,49 @@ func (sa *Application) tryRequiredNode(request 
*Allocation, getNodeFn func(strin
        return result
 }
 
-// cancelReservations will cancel all non required node reservations for a 
node. The list of reservations passed in is
-// a copy of all reservations of a single node. This is called during the 
required node allocation cycle only.
-// The returned int value is used to update the partition counter of active 
reservations.
-func (sa *Application) cancelReservations(reservations []*reservation) int {
-       var released, num int
-       // un reserve all the apps that were reserved on the node
+// unreserveForApp handles the lock-aware unreserve for a single reservation.
+// Uses the internal unlocked path when the reservation belongs to this app.
+func (sa *Application) unreserveForApp(res *reservation) int {
+       var num int
+       if sa.ApplicationID == res.appID {
+               num = sa.unReserveInternal(res)
+               sa.queue.UnReserve(sa.ApplicationID, num)
+       } else {
+               num = res.app.UnReserve(res.node, res.alloc)
+               res.app.GetQueue().UnReserve(res.app.ApplicationID, num)
+       }
+       if num > 0 {
+               log.Log(log.SchedApplication).Info("Reservation cancelled",
+                       zap.String("triggeringAppID", sa.ApplicationID),
+                       zap.String("reservingAppID", res.appID),
+                       zap.String("reservingAllocationKey", res.allocKey),
+                       zap.String("node", res.nodeID))
+       }
+       return num
+}
+
+// cancelMatchingReservations cancels reservations that match the predicate.
+// Returns the number of reservations released and the number remaining.
+func (sa *Application) cancelMatchingReservations(reservations []*reservation, 
shouldCancel func(*reservation) bool) (released, remaining int) {
+       remaining = len(reservations)
        for _, res := range reservations {
-               // cleanup if the reservation does not have this node as a 
requirement
-               if res.alloc.requiredNode != "" {
+               if !shouldCancel(res) {
                        continue
                }
-               thisApp := res.app.ApplicationID == sa.ApplicationID
-               if thisApp {
-                       num = sa.unReserveInternal(res)
-                       sa.queue.UnReserve(sa.ApplicationID, num)
-               } else {
-                       num = res.app.UnReserve(res.node, res.alloc)
-                       res.app.GetQueue().UnReserve(res.app.ApplicationID, num)
-               }
-               log.Log(log.SchedApplication).Info("Cancelled reservation for 
required node allocation",
-                       zap.String("triggered by appID", sa.ApplicationID),
-                       zap.String("affected application ID", res.appID),
-                       zap.String("affected allocationKey", res.allocKey),
-                       zap.String("required node", res.nodeID),
-                       zap.Int("reservations count", num))
+               num := sa.unreserveForApp(res)
                released += num
+               remaining -= num
        }
+       return
+}
+
+// cancelReservations will cancel all non required node reservations for a 
node. The list of reservations passed in is
+// a copy of all reservations of a single node. This is called during the 
required node allocation cycle only.
+// The returned int value is used to update the partition counter of active 
reservations.
+func (sa *Application) cancelReservations(reservations []*reservation) int {
+       released, _ := sa.cancelMatchingReservations(reservations, func(res 
*reservation) bool {
+               return res.alloc.requiredNode == ""
+       })
        return released
 }
 
diff --git a/pkg/scheduler/objects/application_test.go 
b/pkg/scheduler/objects/application_test.go
index fe65d485..0f237df6 100644
--- a/pkg/scheduler/objects/application_test.go
+++ b/pkg/scheduler/objects/application_test.go
@@ -4332,3 +4332,101 @@ func TestReservationReleasedCallback(t *testing.T) {
                t.Fatal("callback was not called")
        }
 }
+
+func TestUnreserveForApp(t *testing.T) {
+       resMap := map[string]string{"first": "10"}
+       rootQ, err := createRootQueue(resMap)
+       assert.NilError(t, err)
+       childQ, err := createManagedQueue(rootQ, "child", false, resMap)
+       assert.NilError(t, err)
+
+       node := newNode(nodeID1, map[string]resources.Quantity{"first": 10})
+       allocRes := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 3})
+
+       // app1 holds the reservation, app2 is the triggering app
+       app1 := newApplication(appID1, "default", childQ.QueuePath)
+       app1.SetQueue(childQ)
+       childQ.applications[appID1] = app1
+       ask1 := newAllocationAsk(aKey, appID1, allocRes)
+       err = app1.AddAllocationAsk(ask1)
+       assert.NilError(t, err)
+       err = app1.reserveInternal(node, ask1)
+       assert.NilError(t, err)
+
+       app2 := newApplication(appID2, "default", childQ.QueuePath)
+       app2.SetQueue(childQ)
+       childQ.applications[appID2] = app2
+
+       // different app: unreserveForApp uses the locked UnReserve path
+       res := app1.reservations[aKey]
+       assert.Assert(t, res != nil)
+       num := app2.unreserveForApp(res)
+       assert.Equal(t, num, 1, "expected reservation to be released")
+       assert.Equal(t, app1.NodeReservedForAsk(aKey), "", "reservation should 
be removed from app1")
+
+       // same app: re-reserve and use same-app path
+       err = app1.reserveInternal(node, ask1)
+       assert.NilError(t, err)
+       res = app1.reservations[aKey]
+       assert.Assert(t, res != nil)
+       num = app1.unreserveForApp(res)
+       assert.Equal(t, num, 1, "expected reservation to be released via 
internal path")
+       assert.Equal(t, app1.NodeReservedForAsk(aKey), "", "reservation should 
be removed")
+}
+
+func TestCancelMatchingReservations(t *testing.T) {
+       resMap := map[string]string{"first": "10"}
+       rootQ, err := createRootQueue(resMap)
+       assert.NilError(t, err)
+       childQ, err := createManagedQueue(rootQ, "child", false, resMap)
+       assert.NilError(t, err)
+
+       node1 := newNode(nodeID1, map[string]resources.Quantity{"first": 10})
+       node2 := newNode(nodeID2, map[string]resources.Quantity{"first": 10})
+       allocRes := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 2})
+
+       app := newApplication(appID1, "default", childQ.QueuePath)
+       app.SetQueue(childQ)
+       childQ.applications[appID1] = app
+
+       // reserve ask1 on node1, ask2 on node2
+       ask1 := newAllocationAsk(aKey, appID1, allocRes)
+       err = app.AddAllocationAsk(ask1)
+       assert.NilError(t, err)
+       err = app.reserveInternal(node1, ask1)
+       assert.NilError(t, err)
+
+       ask2 := newAllocationAsk(aKey2, appID1, allocRes)
+       err = app.AddAllocationAsk(ask2)
+       assert.NilError(t, err)
+       err = app.reserveInternal(node2, ask2)
+       assert.NilError(t, err)
+
+       // gather both reservations
+       reservations := append(node1.GetReservations(), 
node2.GetReservations()...)
+       assert.Equal(t, len(reservations), 2, "expected 2 reservations total")
+
+       // cancel only the first ask's reservation using predicate on allocKey
+       released, remaining := app.cancelMatchingReservations(reservations, 
func(res *reservation) bool {
+               return res.allocKey == aKey
+       })
+       assert.Equal(t, released, 1, "expected 1 reservation released")
+       assert.Equal(t, remaining, 1, "expected 1 reservation remaining")
+       assert.Equal(t, app.NodeReservedForAsk(aKey), "", "first reservation 
should be gone")
+       assert.Equal(t, app.NodeReservedForAsk(aKey2), nodeID2, "second 
reservation should remain")
+
+       // cancel all remaining
+       reservations = node2.GetReservations()
+       released, remaining = app.cancelMatchingReservations(reservations, 
func(res *reservation) bool {
+               return true
+       })
+       assert.Equal(t, released, 1, "expected 1 reservation released")
+       assert.Equal(t, remaining, 0, "expected no reservations remaining")
+
+       // empty list returns zero
+       released, remaining = app.cancelMatchingReservations(nil, func(res 
*reservation) bool {
+               return true
+       })
+       assert.Equal(t, released, 0)
+       assert.Equal(t, remaining, 0)
+}
diff --git a/pkg/scheduler/objects/preemption.go 
b/pkg/scheduler/objects/preemption.go
index dc791184..211e883b 100644
--- a/pkg/scheduler/objects/preemption.go
+++ b/pkg/scheduler/objects/preemption.go
@@ -165,40 +165,21 @@ func (p *Preemptor) initWorkingState() int {
        p.iterator.ForEachNode(func(node *Node) bool {
                isReserved := false
                if node.IsReserved() && 
!node.isReservedForAllocation(p.ask.GetAllocationKey()) {
-                       leftCount := 0
-                       for _, res := range node.GetReservations() {
-                               leftCount++
-                               // Is Allocation daemon set?
-                               // Has this allocation already triggered 
preemption?
+                       askPriority := p.ask.priority
+                       released, remaining := 
p.application.cancelMatchingReservations(node.GetReservations(), func(res 
*reservation) bool {
                                if res.alloc.requiredNode != "" || 
res.alloc.HasTriggeredPreemption() {
-                                       continue
+                                       return false
                                }
-                               // Cancel reservation based on its priority and 
waiting time in reservation queue
-                               if res.alloc.GetPriority() < p.ask.priority && 
time.Since(res.createTime) > reservationWaitTimeout {
-                                       
log.Log(log.SchedPreemption).Info("Cancelling reservation to consider node for 
preemption",
-                                               zap.String("triggeringAppID", 
p.application.ApplicationID),
-                                               
zap.String("triggeringAllocationKey", p.ask.allocationKey),
-                                               zap.String("reservingAppID", 
res.appID),
-                                               
zap.String("reservingAllocationKey", res.allocKey),
-                                               zap.String("node", node.NodeID))
-                                       num := 0
-                                       if p.application.ApplicationID == 
res.appID {
-                                               num = 
res.app.unReserveInternal(res)
-                                               
res.app.queue.UnReserve(res.app.ApplicationID, num)
-                                       } else {
-                                               num = 
res.app.UnReserve(res.node, res.alloc)
-                                               
res.app.GetQueue().UnReserve(res.app.ApplicationID, num)
-                                       }
-                                       totalReservationCancel += num
-                                       leftCount -= num
-                               }
-                       }
-                       log.Log(log.SchedPreemption).Debug("Reservations left 
on node are cleanup",
+                               return res.alloc.GetPriority() < askPriority && 
time.Since(res.createTime) > reservationWaitTimeout
+                       })
+                       totalReservationCancel += released
+                       log.Log(log.SchedPreemption).Debug("Reservations left 
on node after cleanup",
                                zap.String("triggeringAppID", 
p.application.ApplicationID),
                                zap.String("triggeringAllocationKey", 
p.ask.allocationKey),
                                zap.String("node", node.NodeID),
-                               zap.Int("leftCount", leftCount))
-                       isReserved = leftCount > 0
+                               zap.Int("released", released),
+                               zap.Int("remaining", remaining))
+                       isReserved = remaining > 0
                }
                if !node.IsSchedulable() || isReserved || 
!node.FitInNode(p.ask.GetAllocatedResource()) {
                        // node is not available, remove any potential victims 
from consideration


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to