This is an automated email from the ASF dual-hosted git repository.

mani pushed a commit to branch branch-1.8
in repository https://gitbox.apache.org/repos/asf/yunikorn-core.git


The following commit(s) were added to refs/heads/branch-1.8 by this push:
     new 10f50b2b [YUNIKORN-3113] Resource-wise preemption (#1029) (#1060)
10f50b2b is described below

commit 10f50b2b9b300d7991e95365ccbf8012e495997a
Author: Manikandan R <[email protected]>
AuthorDate: Tue Jan 13 15:46:44 2026 +0530

    [YUNIKORN-3113] Resource-wise preemption (#1029) (#1060)
    
    Introduced FitInActual() method to decide whether the ask would fit into
    the ask queue or not instead of using strict based methods to avoid
    problems due to, from the asks' resource requirement perspective, unreleated
    res types with negative values
    
    Introduced two more methods in Preemptor to decide ask queue is under
    guaranteed and victim queue is over guranateed or not from ask resource
    requirement perspective. Used these methods in appropriate places to 
simplify
    the decision making Added tests for the same.
    
    Closes: #1029
    
    
    (cherry picked from commit 54a2a2566ee27e51c8d28588dc9a884e0ba4b48f)
    
    Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
 pkg/common/resources/resources.go        |  19 +-
 pkg/common/resources/resources_test.go   |  71 ++++++++
 pkg/scheduler/objects/preemption.go      | 118 ++++++++----
 pkg/scheduler/objects/preemption_test.go | 303 ++++++++++++++++++++++++++++++-
 4 files changed, 459 insertions(+), 52 deletions(-)

diff --git a/pkg/common/resources/resources.go 
b/pkg/common/resources/resources.go
index bb3df50a..290ba022 100644
--- a/pkg/common/resources/resources.go
+++ b/pkg/common/resources/resources.go
@@ -446,20 +446,27 @@ func subNonNegative(left, right *Resource) (*Resource, 
string) {
 // Types not defined in resource this is called against are considered 0 for 
Quantity
 // A nil resource is treated as an empty resource (no types defined)
 func (r *Resource) FitIn(smaller *Resource) bool {
-       return r.fitIn(smaller, false)
+       return r.fitIn(smaller, false, false)
 }
 
 // FitInMaxUndef checks if smaller fits in the defined resource
 // Types not defined in resource this is called against are considered the 
maximum value for Quantity
 // A nil resource is treated as an empty resource (no types defined)
 func (r *Resource) FitInMaxUndef(smaller *Resource) bool {
-       return r.fitIn(smaller, true)
+       return r.fitIn(smaller, true, false)
+}
+
+// FitInActual checks if smaller fits in the defined resource based on the 
actual values. Negative values too are compared as is.
+// Types not defined in resource this is called against are skipped
+// A nil resource is treated as an empty resource (no types defined)
+func (r *Resource) FitInActual(smaller *Resource) bool {
+       return r.fitIn(smaller, true, true)
 }
 
 // Check if smaller fits in the defined resource
-// Negative values will be treated as 0
+// Negative values will be treated as 0 if actual flag is false. Otherwise, 
use the value as is and do the comparison later
 // A nil resource is treated as an empty resource, behaviour defined by 
skipUndef
-func (r *Resource) fitIn(smaller *Resource, skipUndef bool) bool {
+func (r *Resource) fitIn(smaller *Resource, skipUndef bool, actual bool) bool {
        if r == nil {
                r = Zero // shadows in the local function not seen by the 
callers.
        }
@@ -475,7 +482,9 @@ func (r *Resource) fitIn(smaller *Resource, skipUndef bool) 
bool {
                if skipUndef && !ok {
                        continue
                }
-               largerValue = max(0, largerValue)
+               if !actual {
+                       largerValue = max(0, largerValue)
+               }
                if v > largerValue {
                        return false
                }
diff --git a/pkg/common/resources/resources_test.go 
b/pkg/common/resources/resources_test.go
index 4375a076..5c3b844d 100644
--- a/pkg/common/resources/resources_test.go
+++ b/pkg/common/resources/resources_test.go
@@ -1381,6 +1381,77 @@ func TestFitInSkip(t *testing.T) {
        }
 }
 
+func TestFitInActual(t *testing.T) {
+       tests := []struct {
+               larger   *Resource
+               smaller  *Resource
+               expected bool
+               message  string
+       }{
+               {
+                       larger:   NewResource(),
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
1}},
+                       expected: true,
+                       message:  "defined resource %+v should fit in empty 
(skip undefined)",
+               },
+               {
+                       larger:   NewResourceFromMap(map[string]Quantity{"a": 
5}),
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
1}},
+                       expected: true,
+                       message:  "fitin smaller resource with value %+v should 
fit in larger %+v (skip undefined)",
+               },
+               {
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
1}},
+                       larger:   &Resource{Resources: 
map[string]Quantity{"not-in-smaller": 1}},
+                       expected: true,
+                       message:  "different type in smaller %+v should fit in 
larger %+v (skip undefined)",
+               },
+               {
+                       larger:   &Resource{Resources: 
map[string]Quantity{"not-in-smaller": 1}},
+                       smaller:  &Resource{Resources: 
map[string]Quantity{"not-in-larger": 1}},
+                       expected: true,
+                       message:  "different type in smaller %+v should fit in 
larger %+v (skip undefined)",
+               },
+               {
+                       larger:   &Resource{Resources: map[string]Quantity{"a": 
-10}},
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
0, "b": -10}},
+                       expected: false,
+                       message:  "fitin smaller resource with zero or neg 
values %+v should not fit in larger %+v (skip undefined)",
+               },
+               {
+                       larger:   &Resource{Resources: map[string]Quantity{"a": 
-5}},
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
0, "b": 10}},
+                       expected: false,
+                       message:  "fitin smaller resource with value %+v should 
not fit in larger %+v (skip undefined)",
+               },
+               {
+                       larger:   &Resource{Resources: map[string]Quantity{"a": 
-5}},
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
-4, "b": 10}},
+                       expected: false,
+                       message:  "fitin smaller resource with lesser neg value 
%+v should not fit in larger %+v (skip undefined)",
+               },
+               {
+                       larger:   &Resource{Resources: map[string]Quantity{"a": 
-5}},
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
-6, "b": 10}},
+                       expected: true,
+                       message:  "fitin smaller resource with higher neg value 
%+v should fit in larger %+v (skip undefined)",
+               },
+               {
+                       larger:   &Resource{Resources: map[string]Quantity{"a": 
-5}},
+                       smaller:  &Resource{Resources: map[string]Quantity{"a": 
-5, "b": 10}},
+                       expected: true,
+                       message:  "fitin smaller resource with equal neg value 
%+v should fit in larger %+v (skip undefined)",
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.message, func(t *testing.T) {
+                       result := tc.larger.FitInActual(tc.smaller)
+                       assert.Equal(t, result, tc.expected, tc.message, 
tc.smaller, tc.larger)
+               })
+       }
+}
+
 //nolint:funlen // thorough test
 func TestGetFairShare(t *testing.T) {
        // 0 guarantee should be treated as absence of a gurantee
diff --git a/pkg/scheduler/objects/preemption.go 
b/pkg/scheduler/objects/preemption.go
index f2aa91dc..4849c8d0 100644
--- a/pkg/scheduler/objects/preemption.go
+++ b/pkg/scheduler/objects/preemption.go
@@ -218,7 +218,10 @@ func (p *Preemptor) checkPreemptionQueueGuarantees() bool {
                        zap.String("queuePath", p.queuePath))
                return false
        }
-
+       oldRemaining := currentQueue.GetRemainingGuaranteedResource()
+       if oldRemaining != nil && 
oldRemaining.FitInActual(p.ask.GetAllocatedResource()) {
+               return true
+       }
        currentQueue.AddAllocation(p.ask.GetAllocatedResource())
 
        // remove each allocation in turn, validating that at some point we 
free enough resources to allow this ask to fit
@@ -226,7 +229,9 @@ func (p *Preemptor) checkPreemptionQueueGuarantees() bool {
                for _, alloc := range snapshot.PotentialVictims {
                        snapshot.RemoveAllocation(alloc.GetAllocatedResource())
                        remaining := 
currentQueue.GetRemainingGuaranteedResource()
-                       if remaining != nil && 
resources.StrictlyGreaterThanOrEquals(remaining, resources.Zero) {
+
+                       // Is all ask's res types in ask queue still under 
guaranteed?
+                       if remaining != nil && 
isAskQueueUnderGuaranteed(p.ask.GetAllocatedResource(), remaining) {
                                return true
                        }
                }
@@ -268,26 +273,24 @@ func (p *Preemptor) calculateVictimsByNode(nodeAvailable 
*resources.Resource, po
                // check to see if removing this task will keep queue above 
guaranteed amount; if not, skip to the next one
                if qv, ok := p.queueByAlloc[victim.GetAllocationKey()]; ok {
                        if queueSnapshot, ok2 := 
allocationsByQueueSnap[qv.QueuePath]; ok2 {
-                               oldRemaining := 
queueSnapshot.GetRemainingGuaranteedResource()
+                               remaining := 
queueSnapshot.GetRemainingGuaranteedResource()
                                
queueSnapshot.RemoveAllocation(victim.GetAllocatedResource())
                                preemptableResource := 
queueSnapshot.GetPreemptableResource()
 
-                               // Did removing this allocation still keep the 
queue over-allocated?
+                               // Did removing this allocation still keep the 
victim queue over-allocated?
                                // At times, over-allocation happens because of 
resource types in usage but not defined as guaranteed.
-                               // So, as an additional check, -ve remaining 
guaranteed resource before removing the victim means
-                               // some really useful victim is there.
+                               // So, as an additional check, res types used 
by ask should be either -ve or zero in victim queue remaining guaranteed 
resource to confirm
+                               // some relevant useful victim is there.
                                // In case of victims densely populated on any 
specific node, checking/honouring the guaranteed quota on ask or preemptor queue
                                // acts as early filtering layer to carry 
forward only the required victims.
                                // For other cases like victims spread over 
multiple nodes, this doesn't add great value.
                                if 
resources.StrictlyGreaterThanOrEquals(preemptableResource, resources.Zero) &&
-                                       (oldRemaining == nil || 
resources.StrictlyGreaterThan(resources.Zero, oldRemaining)) {
-                                       // add the current victim into the ask 
queue
-                                       
askQueue.AddAllocation(victim.GetAllocatedResource())
-                                       askQueueNewRemaining := 
askQueue.GetRemainingGuaranteedResource()
-
-                                       // Did adding this allocation make the 
ask queue over - utilized?
-                                       if askQueueNewRemaining != nil && 
resources.StrictlyGreaterThan(resources.Zero, askQueueNewRemaining) {
-                                               
askQueue.RemoveAllocation(victim.GetAllocatedResource())
+                                       (remaining == nil || 
isVictimQueueOverGuaranteed(p.ask.GetAllocatedResource(), remaining)) {
+                                       // Does victimQueue have space 
equivalent to the resource used by the victim?
+                                       askQueueRemaining := 
askQueue.GetRemainingGuaranteedResource()
+                                       if askQueueRemaining != nil && 
askQueueRemaining.FitInActual(victim.GetAllocatedResource()) {
+                                               
askQueue.AddAllocation(victim.GetAllocatedResource())
+                                       } else {
                                                
queueSnapshot.AddAllocation(victim.GetAllocatedResource())
                                                break
                                        }
@@ -340,17 +343,17 @@ func (p *Preemptor) calculateVictimsByNode(nodeAvailable 
*resources.Resource, po
                // check to see if removing this task will keep queue above 
guaranteed amount; if not, skip to the next one
                if qv, ok := p.queueByAlloc[victim.GetAllocationKey()]; ok {
                        if queueSnapshot, ok2 := 
allocationsByQueueSnap[qv.QueuePath]; ok2 {
-                               oldRemaining := 
queueSnapshot.GetRemainingGuaranteedResource()
+                               remaining := 
queueSnapshot.GetRemainingGuaranteedResource()
                                
queueSnapshot.RemoveAllocation(victim.GetAllocatedResource())
                                preemptableResource := 
queueSnapshot.GetPreemptableResource()
 
-                               // Did removing this allocation still keep the 
queue over-allocated?
+                               // Did removing this allocation still keep the 
victim queue over-allocated?
                                // At times, over-allocation happens because of 
resource types in usage but not defined as guaranteed.
-                               // So, as an additional check, -ve remaining 
guaranteed resource before removing the victim means
-                               // some really useful victim is there.
+                               // So, as an additional check, res types used 
by ask should be either -ve or zero in victim queue remaining guaranteed 
resource to confirm
+                               // some relevant useful victim is there.
                                // Similar checks could be added even on the 
ask or preemptor queue to prevent being over utilized.
                                if 
resources.StrictlyGreaterThanOrEquals(preemptableResource, resources.Zero) &&
-                                       (oldRemaining == nil || 
resources.StrictlyGreaterThan(resources.Zero, oldRemaining)) {
+                                       (remaining == nil || 
isVictimQueueOverGuaranteed(p.ask.GetAllocatedResource(), remaining)) {
                                        // removing task does not violate queue 
constraints, adjust queue and node
                                        
nodeCurrentAvailable.AddTo(victim.GetAllocatedResource())
                                        // check if ask now fits and we haven't 
had this happen before
@@ -499,29 +502,28 @@ func (p *Preemptor) 
calculateAdditionalVictims(nodeVictims []*Allocation) ([]*Al
                // check to see if removing this task will keep queue above 
guaranteed amount; if not, skip to the next one
                if qv, ok := p.queueByAlloc[victim.GetAllocationKey()]; ok {
                        if queueSnapshot, ok2 := 
allocationsByQueueSnap[qv.QueuePath]; ok2 {
-                               oldRemaining := 
queueSnapshot.GetRemainingGuaranteedResource()
+                               remaining := 
queueSnapshot.GetRemainingGuaranteedResource()
                                
queueSnapshot.RemoveAllocation(victim.GetAllocatedResource())
 
-                               // Did removing this allocation still keep the 
queue over-allocated?
+                               // Did removing this allocation still keep the 
victim queue over-allocated?
                                // At times, over-allocation happens because of 
resource types in usage but not defined as guaranteed.
-                               // So, as an additional check, -ve remaining 
guaranteed resource before removing the victim means
-                               // some really useful victim is there.
+                               // So, as an additional check, res types used 
by ask should be either -ve or zero in victim queue remaining guaranteed 
resource to confirm
+                               // some relevant useful victim is there.
                                preemptableResource := 
queueSnapshot.GetPreemptableResource()
                                if 
resources.StrictlyGreaterThanOrEquals(preemptableResource, resources.Zero) &&
-                                       (oldRemaining == nil || 
resources.StrictlyGreaterThan(resources.Zero, oldRemaining)) {
-                                       askQueueRemainingAfterVictimRemoval := 
askQueue.GetRemainingGuaranteedResource()
-
-                                       // add the current victim into the ask 
queue
-                                       
askQueue.AddAllocation(victim.GetAllocatedResource())
-                                       askQueueNewRemaining := 
askQueue.GetRemainingGuaranteedResource()
-                                       // Did adding this allocation make the 
ask queue over - utilized?
-                                       if askQueueNewRemaining != nil && 
resources.StrictlyGreaterThan(resources.Zero, askQueueNewRemaining) {
-                                               
askQueue.RemoveAllocation(victim.GetAllocatedResource())
+                                       (remaining == nil || 
isVictimQueueOverGuaranteed(p.ask.GetAllocatedResource(), remaining)) {
+                                       // Does victimQueue have space 
equivalent to the resource used by the victim?
+                                       askQueueRemaining := 
askQueue.GetRemainingGuaranteedResource()
+                                       if askQueueRemaining != nil && 
askQueueRemaining.FitInActual(victim.GetAllocatedResource()) {
+                                               
askQueue.AddAllocation(victim.GetAllocatedResource())
+                                       } else {
                                                
queueSnapshot.AddAllocation(victim.GetAllocatedResource())
                                                break
                                        }
+                                       askQueueNewRemaining := 
askQueue.GetRemainingGuaranteedResource()
+
                                        // check to see if the shortfall on the 
queue has changed
-                                       if 
!resources.EqualsOrEmpty(askQueueRemainingAfterVictimRemoval, 
askQueueNewRemaining) {
+                                       if 
!resources.EqualsOrEmpty(askQueueRemaining, askQueueNewRemaining) {
                                                // remaining capacity changed, 
so we should keep this task
                                                victims = append(victims, 
victim)
                                        } else {
@@ -536,12 +538,17 @@ func (p *Preemptor) 
calculateAdditionalVictims(nodeVictims []*Allocation) ([]*Al
                        }
                }
        }
-       // At last, did the ask queue usage under or equals guaranteed quota?
-       finalRemainingRes := askQueue.GetRemainingGuaranteedResource()
-       if finalRemainingRes != nil && 
resources.StrictlyGreaterThanOrEquals(finalRemainingRes, resources.Zero) {
-               return victims, true
+
+       // At last, did the ask queue usage under or equals guaranteed quota 
after finding the additional victims?
+       if len(victims) > 0 {
+               finalRemainingRes := askQueue.GetRemainingGuaranteedResource()
+               if finalRemainingRes != nil && 
isAskQueueUnderGuaranteed(p.ask.GetAllocatedResource(), finalRemainingRes) {
+                       return victims, true
+               } else {
+                       return victims, false
+               }
        }
-       return nil, false
+       return nil, true
 }
 
 // tryNodes attempts to find potential nodes for scheduling. For each node, 
potential victims are passed to
@@ -678,7 +685,7 @@ func (p *Preemptor) TryPreemption() (*AllocationResult, 
bool) {
                        log.Log(log.SchedPreemption).Info("Preempting task",
                                zap.String("askApplicationID", 
p.ask.applicationID),
                                zap.String("askAllocationKey", 
p.ask.allocationKey),
-                               zap.String("askQueue", p.queue.Name),
+                               zap.String("victimQueue", p.queue.Name),
                                zap.String("victimApplicationID", 
victim.GetApplicationID()),
                                zap.String("victimAllocationKey", 
victim.GetAllocationKey()),
                                zap.Stringer("victimAllocatedResource", 
victim.GetAllocatedResource()),
@@ -705,7 +712,8 @@ func (p *Preemptor) TryPreemption() (*AllocationResult, 
bool) {
        log.Log(log.SchedPreemption).Info("Reserving node for ask after 
preemption",
                zap.String("allocationKey", p.ask.GetAllocationKey()),
                zap.String("nodeID", nodeID),
-               zap.Int("victimCount", len(victims)))
+               zap.Int("collected victim count", len(victims)),
+               zap.Int("preempted victim count", len(finalVictims)))
        return newReservedAllocationResult(nodeID, p.ask), true
 }
 
@@ -914,3 +922,33 @@ func batchPreemptionChecks(checks 
[]*si.PreemptionPredicatesArgs, batchSize int)
        }
        return result
 }
+
+// isAskQueueUnderGuaranteed Is Ask Queue (not in general sense) under 
guaranteed purely based on the ask's resource requirement?
+// Traverse each ask's res type, confirm its existence in ask queue and check 
whether it has -ve or not.
+// -ve value means over guaranteed, return false to confirm the same
+// For all other cases (even if non-matching res type has -ve value), return 
true
+func isAskQueueUnderGuaranteed(askResource *resources.Resource, askQueue 
*resources.Resource) bool {
+       for resType := range askResource.Resources {
+               if val, ok := askQueue.Resources[resType]; ok {
+                       if val < 0 {
+                               return false
+                       }
+               }
+       }
+       return true
+}
+
+// isVictimQueueOverGuaranteed Is Victim Queue (not in general sense) over 
guaranteed purely based on the ask's resource requirement?
+// Traverse each ask's res type, confirm its existence in victim queue and 
check whether it has -ve or not.
+// -ve value means over guaranteed, return true to confirm the same
+// For all other cases (even if non-matching res type has -ve value), return 
false
+func isVictimQueueOverGuaranteed(askResource *resources.Resource, victimQueue 
*resources.Resource) bool {
+       for resType := range askResource.Resources {
+               if val, ok := victimQueue.Resources[resType]; ok {
+                       if val < 0 {
+                               return true
+                       }
+               }
+       }
+       return false
+}
diff --git a/pkg/scheduler/objects/preemption_test.go 
b/pkg/scheduler/objects/preemption_test.go
index 017c9395..599e0f66 100644
--- a/pkg/scheduler/objects/preemption_test.go
+++ b/pkg/scheduler/objects/preemption_test.go
@@ -44,7 +44,17 @@ func creatApp1(
        app1Rec map[string]resources.Quantity,
        appQueueMapping *AppQueueMapping,
 ) (*Allocation, *Allocation, error) {
-       app1 := newApplication(appID1, "default", "root.parent.child1")
+       return creatApp1WithTwoDifferentAllocations(childQ1, node1, node2, 
app1Rec, app1Rec, appQueueMapping)
+}
+
+func creatApp1WithTwoDifferentAllocations(
+       childQ1 *Queue,
+       node1 *Node,
+       node2 *Node,
+       app1Rec map[string]resources.Quantity,
+       app2Rec map[string]resources.Quantity, appQueueMapping *AppQueueMapping,
+) (*Allocation, *Allocation, error) {
+       app1 := newApplication(appID1, "default", childQ1.QueuePath)
        app1.SetQueue(childQ1)
        childQ1.AddApplication(app1)
        appQueueMapping.AddAppQueueMapping(app1.ApplicationID, childQ1)
@@ -54,7 +64,7 @@ func creatApp1(
        if err := app1.AddAllocationAsk(ask1); err != nil {
                return nil, nil, err
        }
-       ask2 := newAllocationAsk("alloc2", appID1, 
resources.NewResourceFromMap(app1Rec))
+       ask2 := newAllocationAsk("alloc2", appID1, 
resources.NewResourceFromMap(app2Rec))
        ask2.createTime = time.Now()
        if err := app1.AddAllocationAsk(ask2); err != nil {
                return nil, nil, err
@@ -68,14 +78,14 @@ func creatApp1(
        }
        var alloc2 *Allocation
        if node2 != nil {
-               alloc2 = newAllocationWithKey("alloc2", appID1, nodeID2, 
resources.NewResourceFromMap(app1Rec))
+               alloc2 = newAllocationWithKey("alloc2", appID1, nodeID2, 
resources.NewResourceFromMap(app2Rec))
                alloc2.createTime = ask2.createTime
                app1.AddAllocation(alloc2)
                if !node2.TryAddAllocation(alloc2) {
                        return nil, nil, fmt.Errorf("node alloc2 failed")
                }
        } else {
-               alloc2 = newAllocationWithKey("alloc2", appID1, nodeID1, 
resources.NewResourceFromMap(app1Rec))
+               alloc2 = newAllocationWithKey("alloc2", appID1, nodeID1, 
resources.NewResourceFromMap(app2Rec))
                alloc2.createTime = ask2.createTime
                if !node1.TryAddAllocation(alloc2) {
                        return nil, nil, fmt.Errorf("node alloc2 failed")
@@ -98,7 +108,7 @@ func creatApp2(
        allocID string,
        appQueueMapping *AppQueueMapping,
 ) (*Application, *Allocation, error) {
-       app2 := newApplication(appID2, "default", "root.parent.child2")
+       app2 := newApplication(appID2, "default", childQ2.QueuePath)
        app2.SetQueue(childQ2)
        childQ2.AddApplication(app2)
        appQueueMapping.AddAppQueueMapping(app2.ApplicationID, childQ2)
@@ -226,7 +236,8 @@ func 
TestCheckPreemptionQueueGuaranteesWithNoGuaranteedResources(t *testing.T) {
                t.Run(tt.testName, func(t *testing.T) {
                        appQueueMapping := NewAppQueueMapping()
                        node := newNode("node1", 
map[string]resources.Quantity{"first": 20})
-                       iterator := getNodeIteratorFn(node)
+                       node2 := newNode("node2", 
map[string]resources.Quantity{"first": 20})
+                       iterator := getNodeIteratorFn(node, node2)
                        rootQ, err := 
createRootQueue(map[string]string{"first": "20"})
                        assert.NilError(t, err)
                        parentQ, err := createManagedQueueGuaranteed(rootQ, 
"parent", true, map[string]string{}, tt.parentGuaranteed, appQueueMapping)
@@ -236,7 +247,7 @@ func 
TestCheckPreemptionQueueGuaranteesWithNoGuaranteedResources(t *testing.T) {
                        childQ2, err := createManagedQueueGuaranteed(parentQ, 
"child2", false, map[string]string{}, tt.childGuaranteed, appQueueMapping)
                        assert.NilError(t, err)
 
-                       alloc1, alloc2, err := creatApp1(childQ1, node, nil, 
map[string]resources.Quantity{"first": 5}, appQueueMapping)
+                       alloc1, alloc2, err := creatApp1(childQ1, node, node2, 
map[string]resources.Quantity{"first": 5}, appQueueMapping)
                        assert.NilError(t, err)
                        assert.Assert(t, alloc1 != nil, "alloc1 should not be 
nil")
                        assert.Assert(t, alloc2 != nil, "alloc2 should not be 
nil")
@@ -261,6 +272,54 @@ func 
TestCheckPreemptionQueueGuaranteesWithNoGuaranteedResources(t *testing.T) {
        }
 }
 
+func TestIsAskQueueUnderGuaranteed(t *testing.T) {
+       var tests = []struct {
+               testName    string
+               askResource *resources.Resource
+               askQueue    *resources.Resource
+               expected    bool
+       }{
+               {"matching res types with +ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10}), true},
+               {"matching res types with zero value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 0}), true},
+               {"matching res types with -ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -10}), 
false},
+               {"matching res types, one with -ve value and another with +ve 
value", resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"second": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": -10}), false},
+               {"both matching res types  with +ve value and non matching res 
types with -ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": -10}), true},
+               {"both matching res types  with -ve value and non matching res 
types with +ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -10, 
"second": 10}), false},
+               {"both matching res types  with +ve value and non matching res 
types", resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"third": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10}), true},
+               {"both matching res types  with -ve value and non matching res 
types", resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"third": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -10}), 
false},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.testName, func(t *testing.T) {
+                       assert.Equal(t, 
isAskQueueUnderGuaranteed(tt.askResource, tt.askQueue), tt.expected)
+               })
+       }
+}
+
+func TestIsVictimQueueOverGuaranteed(t *testing.T) {
+       var tests = []struct {
+               testName    string
+               askResource *resources.Resource
+               victimQueue *resources.Resource
+               expected    bool
+       }{
+               {"matching res types with +ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10}), 
false},
+               {"matching res types with zero value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 0}), false},
+               {"matching res types with -ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -10}), 
true},
+               {"matching res types, one with -ve value and another with +ve 
value", resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"second": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": -10}), true},
+               {"both matching res types  with +ve value and non matching res 
types with -ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": -10}), false},
+               {"both matching res types  with -ve value and non matching res 
types with +ve value", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -10, 
"second": 10}), true},
+               {"both matching res types  with +ve value and non matching res 
types", resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"third": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10}), 
false},
+               {"both matching res types  with -ve value and non matching res 
types", resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"third": 1}), 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -10}), 
true},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.testName, func(t *testing.T) {
+                       assert.Equal(t, 
isVictimQueueOverGuaranteed(tt.askResource, tt.victimQueue), tt.expected)
+               })
+       }
+}
+
 func TestTryPreemption(t *testing.T) {
        appQueueMapping := NewAppQueueMapping()
        node := newNode(nodeID1, map[string]resources.Quantity{"first": 10, 
"pods": 5})
@@ -1957,3 +2016,233 @@ func 
TestTryPreemption_OnNode_UGParent_With_UGPreemptorChild_OGVictimChild_As_Si
        assert.Check(t, !alloc1.IsPreempted(), "alloc1 preempted")
        assert.Check(t, alloc2.IsPreempted(), "alloc2 not preempted")
 }
+
+// TestTryPreemption_VictimQueue_With_OG_And_UG_ResTypes Test try preemption 
with 2 level queue hierarchy.
+// Guaranteed set on both victim queue path and preemptor queue path.
+// Victim queue has used some res types only but not all defined in guaranteed.
+// Request (Preemptor) resource type matches with victim's queue used resource 
types, but not all the resource types defined in guaranteed.
+// Though not all res types defined in victim's guaranteed not being used and 
not has any relevance from preemptor ask resource requirement perspective, 
preemption should
+// be triggerred and consider the victim queue as candidate and kill the 
victims whose resource type matches with ask res types.
+// Setup:
+// Nodes are Node1 & Node2. Node has enough space to accommodate the new ask.
+// root.parent.child1. Guaranteed set on root.parent.child1, first: 10, 
second: 2 Allocations (belongs to single app) are running. Each Allocation 
usage is first:10. Total usage is first:20.
+// root.parent.child2. Guaranteed set on root.parent.child2, first: 10. 
Request of first: 5 is waiting for resources.
+// 1 Allocation on root.parent.child1 should be preempted to free up resources 
for ask arrived in root.parent.child2 even though some guaranteed resource 
types is not being used at all.
+func TestTryPreemption_VictimQueue_With_OG_And_UG_ResTypes(t *testing.T) {
+       node1 := newNode(nodeID1, map[string]resources.Quantity{"first": 10, 
"second": 2})
+       node2 := newNode(nodeID2, map[string]resources.Quantity{"first": 10, 
"second": 2})
+       iterator := getNodeIteratorFn(node1, node2)
+       appQueueMapping := NewAppQueueMapping()
+       rootQ, err := createRootQueue(map[string]string{"first": "20", 
"storage": "4"})
+       assert.NilError(t, err)
+       parentQ, err := createManagedQueueGuaranteed(rootQ, "parent", true, 
map[string]string{"first": "20", "second": "4"}, nil, appQueueMapping)
+       assert.NilError(t, err)
+       childQ1, err := createManagedQueueGuaranteed(parentQ, "child1", false, 
nil, map[string]string{"first": "10", "second": "2"}, appQueueMapping)
+       assert.NilError(t, err)
+       childQ2, err := createManagedQueueGuaranteed(parentQ, "child2", false, 
nil, map[string]string{"first": "10"}, appQueueMapping)
+       assert.NilError(t, err)
+
+       alloc1, alloc2, err := creatApp1(childQ1, node1, node2, 
map[string]resources.Quantity{"first": 10}, appQueueMapping)
+       assert.NilError(t, err)
+
+       app2, ask3, err := creatApp2(childQ2, 
map[string]resources.Quantity{"first": 5}, "alloc3", appQueueMapping)
+       assert.NilError(t, err)
+
+       headRoom := 
resources.NewResourceFromMap(map[string]resources.Quantity{"second": 4})
+       preemptor := NewPreemptor(app2, headRoom, 30*time.Second, ask3, 
iterator(), false)
+
+       result, ok := preemptor.TryPreemption()
+       assert.Assert(t, result != nil, "unexpected result")
+       assert.Equal(t, ok, true, "victims found")
+       assert.Check(t, alloc1.IsPreempted() || alloc2.IsPreempted(), "alloc1 
or alloc2 preempted")
+}
+
+// TestTryPreemption_VictimQueue_Under_Diff_Parent_With_OG_And_UG_ResTypes 
Test try preemption with 3 level queue hierarchy.
+// Guaranteed set on both victim queue path and preemptor queue path.
+// Victim queue has used some res types only but not all defined in guaranteed.
+// Request (Preemptor) resource type matches with victim's queue used resource 
types, but not all the resource types defined in guaranteed.
+// Though not all res types defined in victim's guaranteed not being used and 
not has any relevance from preemptor ask resource requirement perspective, 
preemption should
+// be triggerred and consider the victim queue as candidate and kill the 
victims whose resource type matches with ask res types.
+// Setup:
+// Nodes are Node1 & Node2. Node has enough space to accommodate the new ask.
+// root.parent.parent1.child1. Guaranteed set on root.parent.parent1.child1, 
first: 10, second: 2 Allocations (belongs to single app) are running. Each 
Allocation usage is first:10. Total usage is first:20.
+// root.parent.parent2.child2. Guaranteed set on root.parent.parent2.child2, 
first: 10. Request of first: 5 is waiting for resources.
+// 1 Allocation on root.parent.parent1.child1 should be preempted to free up 
resources for ask arrived in root.parent.parent2.child2 even though some 
guaranteed resource types of parent2 is not being used at all.
+func TestTryPreemption_VictimQueue_Under_Diff_Parent_With_OG_And_UG_ResTypes(t 
*testing.T) {
+       var tests = []struct {
+               testName               string
+               victimParentGuaranteed map[string]string
+               askParentGuaranteed    map[string]string
+               victimChildGuaranteed  map[string]string
+               askChildGuaranteed     map[string]string
+       }{
+               {"victim queue under parent different from ask queue with some 
OG res types", map[string]string{"first": "10", "second": "2"}, 
map[string]string{"first": "10"}, nil, nil},
+               {"victim queue with some OG res types under parent different 
from ask queue", nil, nil, map[string]string{"first": "10", "second": "2"}, 
map[string]string{"first": "10"}},
+       }
+       for _, tt := range tests {
+               t.Run(tt.testName, func(t *testing.T) {
+                       node1 := newNode(nodeID1, 
map[string]resources.Quantity{"first": 10, "second": 2})
+                       node2 := newNode(nodeID2, 
map[string]resources.Quantity{"first": 10, "second": 2})
+                       iterator := getNodeIteratorFn(node1, node2)
+                       appQueueMapping := NewAppQueueMapping()
+                       rootQ, err := 
createRootQueue(map[string]string{"first": "20", "storage": "4"})
+                       assert.NilError(t, err)
+                       parentQ, err := createManagedQueueGuaranteed(rootQ, 
"parent", true, map[string]string{"first": "20", "second": "4"}, nil, 
appQueueMapping)
+                       assert.NilError(t, err)
+                       parentQ1, err := createManagedQueueGuaranteed(parentQ, 
"parent1", true, nil, tt.victimParentGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+                       parentQ2, err := createManagedQueueGuaranteed(parentQ, 
"parent2", true, nil, tt.askParentGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+                       childQ1, err := createManagedQueueGuaranteed(parentQ1, 
"child1", false, nil, tt.victimChildGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+                       childQ2, err := createManagedQueueGuaranteed(parentQ2, 
"child2", false, nil, tt.askChildGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+
+                       alloc1, alloc2, err := creatApp1(childQ1, node1, node2, 
map[string]resources.Quantity{"first": 10}, appQueueMapping)
+                       assert.NilError(t, err)
+
+                       app2, ask3, err := creatApp2(childQ2, 
map[string]resources.Quantity{"first": 5}, "alloc3", appQueueMapping)
+                       assert.NilError(t, err)
+
+                       headRoom := 
resources.NewResourceFromMap(map[string]resources.Quantity{"second": 4})
+                       preemptor := NewPreemptor(app2, headRoom, 
30*time.Second, ask3, iterator(), false)
+
+                       result, ok := preemptor.TryPreemption()
+                       assert.Assert(t, result != nil, "unexpected result")
+                       assert.Equal(t, ok, true, "victims found")
+                       assert.Check(t, alloc1.IsPreempted() || 
alloc2.IsPreempted(), "alloc1 or alloc2 preempted")
+               })
+       }
+}
+
+// TestTryPreemption_AskQueue_With_OG_And_UG_ResTypes Test try preemption with 
2 level queue hierarchy.
+// Guaranteed set on both victim queue path and preemptor queue path.
+// Ask queue has overused some res types only but not all defined in 
guaranteed, looking for UG resources.
+// Though some res types defined in ask queue's guaranteed has been overused 
and not has any relevance from preemptor ask resource requirement perspective, 
preemption should
+// be triggerred and pass through preliminary checkPreemptionQueueGuarantees 
checks. Just because ask has some OG res types, checkPreemptionQueueGuarantees 
should not fail.
+// Setup:
+// Nodes are Node1 & Node2. Node has enough space to accommodate the new ask.
+// root.parent.child1. Guaranteed set on root.parent.child1, first: 10. 2 
Allocations (belongs to single app) are running. First Allocation usage is 
first:10, Second Allocation usage is first:5. Total usage is first:15.
+// root.parent.child2. Guaranteed set on root.parent.child2, first: 10, 
second: 2. 1 Allocation is running with usage as first:5, second: 3 (OG). 
Request of first: 5, third: 10 is waiting for resources.
+// 1 Allocation on root.parent.child1 should be preempted to free up resources 
for ask arrived in root.parent.child2 even though it has overused few 
guaranteed resource types.
+func TestTryPreemption_AskQueue_With_OG_And_UG_ResTypes(t *testing.T) {
+       node1 := newNode(nodeID1, map[string]resources.Quantity{"first": 10, 
"second": 3, "third": 10})
+       node2 := newNode(nodeID2, map[string]resources.Quantity{"first": 10, 
"second": 3, "third": 10})
+       iterator := getNodeIteratorFn(node1, node2)
+       appQueueMapping := NewAppQueueMapping()
+       rootQ, err := createRootQueue(map[string]string{"first": "20", 
"second": "6"})
+       assert.NilError(t, err)
+       parentQ, err := createManagedQueueGuaranteed(rootQ, "parent", true, 
map[string]string{"first": "20", "second": "6"}, nil, appQueueMapping)
+       assert.NilError(t, err)
+       childQ1, err := createManagedQueueGuaranteed(parentQ, "child1", false, 
nil, map[string]string{"first": "10"}, appQueueMapping)
+       assert.NilError(t, err)
+       childQ2, err := createManagedQueueGuaranteed(parentQ, "child2", false, 
nil, map[string]string{"first": "10", "second": "2"}, appQueueMapping)
+       assert.NilError(t, err)
+
+       alloc1, alloc2, err := creatApp1WithTwoDifferentAllocations(childQ1, 
node1, node2, map[string]resources.Quantity{"first": 10}, 
map[string]resources.Quantity{"first": 5}, appQueueMapping)
+       assert.NilError(t, err)
+
+       alloc3Res := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5, 
"second": 3})
+       app2, ask3, err := creatApp2(childQ2, 
map[string]resources.Quantity{"first": 5, "second": 3}, "alloc3", 
appQueueMapping)
+       assert.NilError(t, err)
+
+       alloc3 := newAllocationWithKey("alloc3", appID2, nodeID2, alloc3Res)
+       alloc3.createTime = ask3.createTime
+       app2.AddAllocation(alloc3)
+       if !node2.TryAddAllocation(alloc3) {
+               t.Fatal("node alloc3 failed")
+       }
+       if err = childQ2.TryIncAllocatedResource(ask3.GetAllocatedResource()); 
err != nil {
+               t.Fatal("inc queue resource failed")
+       }
+       app3 := newApplication(appID3, "default", "root.parent.child2")
+       app3.SetQueue(childQ2)
+       childQ2.applications[appID3] = app3
+
+       ask4 := newAllocationAsk("alloc4", appID3, 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5, "third": 
10}))
+       err = app3.AddAllocationAsk(ask4)
+       assert.NilError(t, err)
+
+       headRoom := 
resources.NewResourceFromMap(map[string]resources.Quantity{"second": 6})
+       preemptor := NewPreemptor(app3, headRoom, 30*time.Second, ask4, 
iterator(), false)
+
+       result, ok := preemptor.TryPreemption()
+       assert.Assert(t, result != nil, "unexpected result")
+       assert.Equal(t, ok, true, "victims found")
+       assert.Check(t, alloc1.IsPreempted() || alloc2.IsPreempted(), "alloc1 
or alloc2 preempted")
+}
+
+// TestTryPreemption_AskQueue_Under_DiffParent_With_OG_And_UG_ResTypes Test 
try preemption with 3 level queue hierarchy.
+// Guaranteed set on both victim queue path and preemptor queue path.
+// Ask queue has overused some res types only but not all defined in 
guaranteed, looking for UG resources.
+// Though some res types defined in ask queue's guaranteed has been overused 
and not has any relevance from preemptor ask resource requirement perspective, 
preemption should
+// be triggerred and pass through preliminary checkPreemptionQueueGuarantees 
checks. Just because ask has some OG res types, checkPreemptionQueueGuarantees 
should not fail.
+// Setup:
+// Nodes are Node1 & Node2. Node has enough space to accommodate the new ask.
+// root.parent.parent1.child1. Guaranteed set on root.parent.parent1.child1, 
first: 10. 2 Allocations (belongs to single app) are running. First Allocation 
usage is first:10, Second Allocation usage is first:5. Total usage is first:15.
+// root.parent.parent2.child2. Guaranteed set on root.parent.parent2.child2, 
first: 10, second: 2. 1 Allocation is running with usage as first:5, second: 3 
(OG). Request of first: 5, third: 10 is waiting for resources.
+// 1 Allocation on root.parent.parent1.child1 should be preempted to free up 
resources for ask arrived in root.parent.parent2.child2 even though it has 
overused few guaranteed resource types.
+func TestTryPreemption_AskQueue_Under_DiffParent_With_OG_And_UG_ResTypes(t 
*testing.T) {
+       var tests = []struct {
+               testName               string
+               victimParentGuaranteed map[string]string
+               askParentGuaranteed    map[string]string
+               victimChildGuaranteed  map[string]string
+               askChildGuaranteed     map[string]string
+       }{
+               {"ask queue under parent different from victim queue with some 
OG res types", map[string]string{"first": "10"}, map[string]string{"first": 
"10", "second": "2"}, nil, nil},
+               {"ask queue with some OG res types under parent different from 
victim queue", nil, nil, map[string]string{"first": "10"}, 
map[string]string{"first": "10", "second": "2"}},
+       }
+       for _, tt := range tests {
+               t.Run(tt.testName, func(t *testing.T) {
+                       node1 := newNode(nodeID1, 
map[string]resources.Quantity{"first": 10, "second": 3, "third": 10})
+                       node2 := newNode(nodeID2, 
map[string]resources.Quantity{"first": 10, "second": 3, "third": 10})
+                       iterator := getNodeIteratorFn(node1, node2)
+                       appQueueMapping := NewAppQueueMapping()
+                       rootQ, err := 
createRootQueue(map[string]string{"first": "20", "second": "6"})
+                       assert.NilError(t, err)
+                       parentQ, err := createManagedQueueGuaranteed(rootQ, 
"parent", true, map[string]string{"first": "20", "second": "6"}, nil, 
appQueueMapping)
+                       assert.NilError(t, err)
+                       parentQ1, err := createManagedQueueGuaranteed(parentQ, 
"parent1", true, nil, tt.victimParentGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+                       parentQ2, err := createManagedQueueGuaranteed(parentQ, 
"parent2", true, nil, tt.askParentGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+                       childQ1, err := createManagedQueueGuaranteed(parentQ1, 
"child1", false, nil, tt.victimChildGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+                       childQ2, err := createManagedQueueGuaranteed(parentQ2, 
"child2", false, nil, tt.askChildGuaranteed, appQueueMapping)
+                       assert.NilError(t, err)
+
+                       alloc1, alloc2, err := 
creatApp1WithTwoDifferentAllocations(childQ1, node1, node2, 
map[string]resources.Quantity{"first": 10}, 
map[string]resources.Quantity{"first": 5}, appQueueMapping)
+                       assert.NilError(t, err)
+
+                       alloc3Res := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5, 
"second": 3})
+                       app2, ask3, err := creatApp2(childQ2, 
map[string]resources.Quantity{"first": 5, "second": 3}, "alloc3", 
appQueueMapping)
+                       assert.NilError(t, err)
+
+                       alloc3 := newAllocationWithKey("alloc3", appID2, 
nodeID2, alloc3Res)
+                       alloc3.createTime = ask3.createTime
+                       app2.AddAllocation(alloc3)
+                       if !node2.TryAddAllocation(alloc3) {
+                               t.Fatal("node alloc3 failed")
+                       }
+                       if err = 
childQ2.TryIncAllocatedResource(ask3.GetAllocatedResource()); err != nil {
+                               t.Fatal("inc queue resource failed")
+                       }
+                       app3 := newApplication(appID3, "default", 
"root.parent.child2")
+                       app3.SetQueue(childQ2)
+                       childQ2.applications[appID3] = app3
+
+                       ask4 := newAllocationAsk("alloc4", appID3, 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5, "third": 
10}))
+                       err = app3.AddAllocationAsk(ask4)
+                       assert.NilError(t, err)
+
+                       headRoom := 
resources.NewResourceFromMap(map[string]resources.Quantity{"second": 6})
+                       preemptor := NewPreemptor(app3, headRoom, 
30*time.Second, ask4, iterator(), false)
+
+                       result, ok := preemptor.TryPreemption()
+                       assert.Assert(t, result != nil, "unexpected result")
+                       assert.Equal(t, ok, true, "victims found")
+                       assert.Check(t, alloc1.IsPreempted() || 
alloc2.IsPreempted(), "alloc1 or alloc2 preempted")
+               })
+       }
+}


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


Reply via email to