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

wilfreds 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 5a40e8ee [YUNIKORN-2197] remove FitInMaxUndef from node code (#735)
5a40e8ee is described below

commit 5a40e8ee01b4b43c9454fb4541e180df0c751c57
Author: Wilfred Spiegelenburg <[email protected]>
AuthorDate: Wed Nov 29 14:48:29 2023 +1100

    [YUNIKORN-2197] remove FitInMaxUndef from node code (#735)
    
    Nodes should perform a strict FitIn for the resources requested not
    allowing resource types in allocation that are not defined on the node.
    Includes start of the Resources.FitIn refactor
    
    This is a followup of the cleanup from YUNIKORN-1125.
    Discovered while testing YUNIKORN-2174.
    
    Closes: #735
    
    Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
 pkg/common/resources/resources.go      | 12 ++++-
 pkg/common/resources/resources_test.go | 53 +++++++------------
 pkg/scheduler/objects/node.go          | 28 +++++-----
 pkg/scheduler/objects/node_test.go     | 94 ++++++++++++++++++++++++++--------
 4 files changed, 118 insertions(+), 69 deletions(-)

diff --git a/pkg/common/resources/resources.go 
b/pkg/common/resources/resources.go
index efa94bc4..7b64fe1a 100644
--- a/pkg/common/resources/resources.go
+++ b/pkg/common/resources/resources.go
@@ -417,14 +417,22 @@ func subNonNegative(left, right *Resource) (*Resource, 
string) {
        return out, message
 }
 
-// Check if smaller fits in larger
+// FitIn Checks if smaller fits in larger
 // Types not defined in the larger resource are considered 0 values for 
Quantity
 // A nil resource is treated as an empty resource (all types are 0)
+// Deprecated: use receiver version Resource.FitIn
 func FitIn(larger, smaller *Resource) bool {
        return larger.fitIn(smaller, false)
 }
 
-// Check if smaller fits in the defined resource
+// FitIn checks if smaller fits in the defined resource
+// 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)
+}
+
+// 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 {
diff --git a/pkg/common/resources/resources_test.go 
b/pkg/common/resources/resources_test.go
index 5c32095c..b3846e6c 100644
--- a/pkg/common/resources/resources_test.go
+++ b/pkg/common/resources/resources_test.go
@@ -1034,42 +1034,27 @@ func TestEqualsOrEmpty(t *testing.T) {
 }
 
 func TestFitIn(t *testing.T) {
-       // simple case (nil checks)
-       empty := NewResource()
-       if !FitIn(nil, empty) {
-               t.Error("fitin empty in nil resource failed")
-       }
-       if !FitIn(empty, nil) {
-               t.Error("fitin nil in empty resource failed")
-       }
-       // zero set resources
-       smaller := &Resource{Resources: map[string]Quantity{"a": 1}}
-       if FitIn(empty, smaller) {
-               t.Errorf("fitin resource with value %v should not fit in 
empty", smaller)
+       tests := []struct {
+               name    string
+               larger  *Resource
+               smaller *Resource
+               want    bool
+       }{
+               {"nil larger", nil, NewResource(), true},
+               {"nil smaller", NewResource(), nil, true},
+               {"zero set", NewResource(), 
NewResourceFromMap(map[string]Quantity{"a": 1}), false},
+               {"same type", NewResourceFromMap(map[string]Quantity{"a": 5}), 
NewResourceFromMap(map[string]Quantity{"a": 1}), true},
+               {"not in smaller", 
NewResourceFromMap(map[string]Quantity{"not-in-smaller": 1}), 
NewResourceFromMap(map[string]Quantity{"a": 1}), false},
+               {"not in larger", 
NewResourceFromMap(map[string]Quantity{"not-in-smaller": 1}), 
NewResourceFromMap(map[string]Quantity{"not-in-larger": 1}), false},
+               {"negative larger", NewResourceFromMap(map[string]Quantity{"a": 
-10}), NewResourceFromMap(map[string]Quantity{"a": 0, "b": -10}), true},
+               {"negative smaller", 
NewResourceFromMap(map[string]Quantity{"a": -5}), 
NewResourceFromMap(map[string]Quantity{"a": 0, "b": 10}), false},
        }
-
-       // simple resources, same type
-       larger := NewResourceFromMap(map[string]Quantity{"a": 5})
-       if !FitIn(larger, smaller) {
-               t.Errorf("fitin smaller resource with value %v should fit in 
larger %v", smaller, larger)
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert.Equal(t, FitIn(tt.larger, tt.smaller), tt.want, 
"unexpected FitIn result (parameter method)")
+                       assert.Equal(t, tt.larger.FitIn(tt.smaller), tt.want, 
"unexpected FitIn result (receiver method)")
+               })
        }
-
-       // check undefined in larger
-       larger = &Resource{Resources: map[string]Quantity{"not-in-smaller": 1}}
-       assert.Assert(t, !FitIn(larger, smaller), "different type in smaller %v 
should not fit in larger %v", smaller, larger)
-
-       // check undefined in smaller
-       smaller = &Resource{Resources: map[string]Quantity{"not-in-larger": 1}}
-       assert.Assert(t, !FitIn(larger, smaller), "different type in smaller %v 
should not fit in larger %v", smaller, larger)
-
-       // complex case: just checking the resource merge with negative values, 
positive is already tested above
-       larger = &Resource{Resources: map[string]Quantity{"a": -10}}
-       smaller = &Resource{Resources: map[string]Quantity{"a": 0, "b": -10}}
-       assert.Assert(t, FitIn(larger, smaller), "fitin smaller resource with 
zero or neg value %v should fit in larger %v", smaller, larger)
-
-       larger = &Resource{Resources: map[string]Quantity{"a": -5}}
-       smaller = &Resource{Resources: map[string]Quantity{"a": 0, "b": 10}}
-       assert.Assert(t, !FitIn(larger, smaller), "fitin smaller resource with 
value %v should not fit in larger %v", smaller, larger)
 }
 
 // simple cases (nil checks)
diff --git a/pkg/scheduler/objects/node.go b/pkg/scheduler/objects/node.go
index cde53ea7..5794c313 100644
--- a/pkg/scheduler/objects/node.go
+++ b/pkg/scheduler/objects/node.go
@@ -284,10 +284,14 @@ func (sn *Node) GetUtilizedResource() *resources.Resource 
{
        return &resources.Resource{Resources: utilizedResource}
 }
 
+// FitInNode checks if the request fits in the node.
+// All resources types requested must match the resource types provided by the 
nodes.
+// A request may ask for only a subset of the types, but the node must provide 
at least the
+// resource types requested in a larger or equal quantity as requested.
 func (sn *Node) FitInNode(resRequest *resources.Resource) bool {
        sn.RLock()
        defer sn.RUnlock()
-       return sn.totalResource.FitInMaxUndef(resRequest)
+       return sn.totalResource.FitIn(resRequest)
 }
 
 // Remove the allocation to the node.
@@ -311,8 +315,8 @@ func (sn *Node) RemoveAllocation(uuid string) *Allocation {
        return nil
 }
 
-// Add the allocation to the node. Used resources will increase available will 
decrease.
-// A nil Allocation makes no changes. Pre-empted resources must have been 
released already.
+// AddAllocation adds the allocation to the node. Used resources will increase 
available will decrease.
+// A nil Allocation makes no changes. Preempted resources must have been 
released already.
 // Do a sanity check to make sure it still fits in the node and nothing has 
changed
 func (sn *Node) AddAllocation(alloc *Allocation) bool {
        if alloc == nil {
@@ -321,9 +325,9 @@ func (sn *Node) AddAllocation(alloc *Allocation) bool {
        defer sn.notifyListeners()
        sn.Lock()
        defer sn.Unlock()
-       // check if this still fits: it might have changed since pre check
+       // check if this still fits: it might have changed since pre-check
        res := alloc.GetAllocatedResource()
-       if sn.availableResource.FitInMaxUndef(res) {
+       if sn.availableResource.FitIn(res) {
                sn.allocations[alloc.GetUUID()] = alloc
                sn.allocatedResource.AddTo(res)
                sn.availableResource.SubFrom(res)
@@ -349,7 +353,7 @@ func (sn *Node) ReplaceAllocation(uuid string, replace 
*Allocation, delta *resou
        // The allocatedResource and availableResource should be updated in the 
same way
        sn.allocatedResource.AddTo(delta)
        sn.availableResource.SubFrom(delta)
-       if !resources.FitIn(before, sn.allocatedResource) {
+       if !before.FitIn(sn.allocatedResource) {
                log.Log(log.SchedNode).Warn("unexpected increase in node usage 
after placeholder replacement",
                        zap.String("placeholder uuid", uuid),
                        zap.String("allocation uuid", replace.GetUUID()),
@@ -357,12 +361,12 @@ func (sn *Node) ReplaceAllocation(uuid string, replace 
*Allocation, delta *resou
        }
 }
 
-// Check if the proposed allocation fits in the available resources.
+// CanAllocate checks if the proposed allocation fits in the available 
resources.
 // If the proposed allocation does not fit false is returned.
 func (sn *Node) CanAllocate(res *resources.Resource) bool {
        sn.RLock()
        defer sn.RUnlock()
-       return sn.availableResource.FitInMaxUndef(res)
+       return sn.availableResource.FitIn(res)
 }
 
 // Checking pre-conditions in the shim for an allocation.
@@ -405,8 +409,8 @@ func (sn *Node) preConditions(ask *AllocationAsk, allocate 
bool) bool {
        return true
 }
 
-// Check if the node should be considered as a possible node to allocate on.
-// This is a lock free call. No updates are made this only performs a pre 
allocate checks
+// preAllocateCheck checks if the node should be considered as a possible node 
to allocate on.
+// No updates are made this only performs a pre allocate checks
 func (sn *Node) preAllocateCheck(res *resources.Resource, resKey string) bool {
        // cannot allocate zero or negative resource
        if !resources.StrictlyGreaterThanZero(res) {
@@ -427,7 +431,7 @@ func (sn *Node) preAllocateCheck(res *resources.Resource, 
resKey string) bool {
        sn.RLock()
        defer sn.RUnlock()
        // returns true/false based on if the request fits in what we have 
calculated
-       return sn.availableResource.FitInMaxUndef(res)
+       return sn.availableResource.FitIn(res)
 }
 
 // Return if the node has been reserved by any application
@@ -479,7 +483,7 @@ func (sn *Node) Reserve(app *Application, ask 
*AllocationAsk) error {
                return fmt.Errorf("reservation creation failed app or ask are 
nil on nodeID %s", sn.NodeID)
        }
        // reservation must fit on the empty node
-       if !resources.FitIn(sn.totalResource, ask.GetAllocatedResource()) {
+       if !sn.totalResource.FitIn(ask.GetAllocatedResource()) {
                log.Log(log.SchedNode).Debug("reservation does not fit on the 
node",
                        zap.String("nodeID", sn.NodeID),
                        zap.String("appID", app.ApplicationID),
diff --git a/pkg/scheduler/objects/node_test.go 
b/pkg/scheduler/objects/node_test.go
index d37d696c..26d33c2f 100644
--- a/pkg/scheduler/objects/node_test.go
+++ b/pkg/scheduler/objects/node_test.go
@@ -130,6 +130,10 @@ func TestPreAllocateCheck(t *testing.T) {
        assert.Assert(t, node.preAllocateCheck(resSmall, ""), "small resource 
should have fitted on node")
        assert.Assert(t, !node.preAllocateCheck(resLarge, ""), "too large 
resource should not have fitted on node")
 
+       // unknown resource type in request is rejected always
+       resOther := 
resources.NewResourceFromMap(map[string]resources.Quantity{"unknown": 1})
+       assert.Assert(t, !node.preAllocateCheck(resOther, ""), "unknown 
resource type should not have fitted on node")
+
        // set allocated resource
        node.AddAllocation(newAllocation(appID1, "UUID1", nodeID, resSmall))
        assert.Assert(t, node.preAllocateCheck(resSmall, ""), "small resource 
should have fitted in available allocation")
@@ -156,19 +160,30 @@ func TestPreAllocateCheck(t *testing.T) {
 
 // Only test the CanAllocate code, the used logic in preAllocateCheck has its 
own test
 func TestCanAllocate(t *testing.T) {
-       node := newNode(nodeID1, map[string]resources.Quantity{"first": 10})
-       if node == nil || node.NodeID != nodeID1 {
-               t.Fatalf("node create failed which should not have %v", node)
-       }
-       // normal alloc
-       res := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
-       if !node.CanAllocate(res) {
-               t.Error("node should have accepted allocation")
-       }
-       // check one that pushes node over its size
-       res = 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 11})
-       if node.CanAllocate(res) {
-               t.Error("node should have rejected allocation (oversize)")
+       available := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": 10})
+       request := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"second": 1})
+       other := 
resources.NewResourceFromMap(map[string]resources.Quantity{"unknown": 1})
+       tests := []struct {
+               name      string
+               available *resources.Resource
+               request   *resources.Resource
+               want      bool
+       }{
+               {"all nils", nil, nil, true},
+               {"nil node available", nil, request, false},
+               {"all matching, req smaller", available, request, true},
+               {"partial match request", available, 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5}), true},
+               {"all matching, req larger", available, resources.Add(request, 
available), false},
+               {"partial match available", available, resources.Add(request, 
other), false},
+               {"unmatched request", available, other, false},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       sn := &Node{
+                               availableResource: tt.available,
+                       }
+                       assert.Equal(t, sn.CanAllocate(tt.request), tt.want, 
"unexpected node can run result")
+               })
        }
 }
 
@@ -193,16 +208,24 @@ func TestNodeReservation(t *testing.T) {
                t.Errorf("illegal reservation requested but did not fail: error 
%v", err)
        }
 
+       // too large for node
        res := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 15})
        ask := newAllocationAsk(aKey, appID1, res)
        app := newApplication(appID1, "default", "root.unknown")
-
-       // too large for node
        err = node.Reserve(app, ask)
        if err == nil {
                t.Errorf("requested reservation does not fit in node resource 
but did not fail: error %v", err)
        }
 
+       // resource type not available on node
+       res = 
resources.NewResourceFromMap(map[string]resources.Quantity{"unknown": 1})
+       ask = newAllocationAsk(aKey, appID1, res)
+       app = newApplication(appID1, "default", "root.unknown")
+       err = node.Reserve(app, ask)
+       if err == nil {
+               t.Errorf("requested reservation does not match node resource 
types but did not fail: error %v", err)
+       }
+
        res = 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
        ask = newAllocationAsk(aKey, appID1, res)
        app = newApplication(appID1, "default", "root.unknown")
@@ -364,13 +387,14 @@ func TestAddAllocation(t *testing.T) {
        }
 
        // check nil alloc
-       node.AddAllocation(nil)
-       if len(node.GetAllAllocations()) > 0 {
-               t.Fatalf("nil allocation should not have been added: %v", node)
-       }
+       assert.Assert(t, !node.AddAllocation(nil), "nil allocation should not 
have been added: %v", node)
+       // check alloc that does not match
+       unknown := 
resources.NewResourceFromMap(map[string]resources.Quantity{"unknown": 1})
+       assert.Assert(t, !node.AddAllocation(newAllocation(appID1, "1", 
nodeID1, unknown)), "unmatched resource type in allocation should not have been 
added: %v", node)
+
        // allocate half of the resources available and check the calculation
        half := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 50, 
"second": 100})
-       node.AddAllocation(newAllocation(appID1, "1", nodeID1, half))
+       assert.Assert(t, node.AddAllocation(newAllocation(appID1, "1", nodeID1, 
half)), "add allocation 1 should not have failed")
        if node.GetAllocation("1") == nil {
                t.Fatal("failed to add allocations: allocation not returned")
        }
@@ -386,7 +410,7 @@ func TestAddAllocation(t *testing.T) {
        }
        // second and check calculation
        piece := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 25, 
"second": 50})
-       node.AddAllocation(newAllocation(appID1, "2", nodeID1, piece))
+       assert.Assert(t, node.AddAllocation(newAllocation(appID1, "2", nodeID1, 
piece)), "add allocation 2 should not have failed")
        if node.GetAllocation("2") == nil {
                t.Fatal("failed to add allocations: allocation not returned")
        }
@@ -747,3 +771,31 @@ func TestNodeEvents(t *testing.T) {
        assert.Equal(t, si.EventRecord_REMOVE, event.EventChangeType)
        assert.Equal(t, si.EventRecord_NODE_RESERVATION, 
event.EventChangeDetail)
 }
+
+func TestNode_FitInNode(t *testing.T) {
+       total := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": 10})
+       request := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"second": 1})
+       other := 
resources.NewResourceFromMap(map[string]resources.Quantity{"unknown": 1})
+       tests := []struct {
+               name       string
+               totalRes   *resources.Resource
+               resRequest *resources.Resource
+               want       bool
+       }{
+               {"all nils", nil, nil, true},
+               {"nil node size", nil, request, false},
+               {"all matching, req smaller", total, request, true},
+               {"partial match request", total, 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5}), true},
+               {"all matching, req larger", total, resources.Add(request, 
total), false},
+               {"partial match total", total, resources.Add(request, other), 
false},
+               {"unmatched request", total, other, false},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       sn := &Node{
+                               totalResource: tt.totalRes,
+                       }
+                       assert.Equal(t, sn.FitInNode(tt.resRequest), tt.want, 
"unexpected node fit result")
+               })
+       }
+}


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

Reply via email to