Merge pull request #93473 from Nordix/sched-fix-mael

Change nodeInfolist building logic in scheduler
This commit is contained in:
Kubernetes Prow Robot 2020-09-07 14:01:44 -07:00 committed by GitHub
commit 2481e88485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 150 deletions

View File

@ -279,9 +279,11 @@ func (cache *schedulerCache) updateNodeInfoSnapshotList(snapshot *Snapshot, upda
if updateAll { if updateAll {
// Take a snapshot of the nodes order in the tree // Take a snapshot of the nodes order in the tree
snapshot.nodeInfoList = make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes) snapshot.nodeInfoList = make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes)
cache.nodeTree.resetExhausted() nodesList, err := cache.nodeTree.list()
for i := 0; i < cache.nodeTree.numNodes; i++ { if err != nil {
nodeName := cache.nodeTree.next() klog.Error(err)
}
for _, nodeName := range nodesList {
if n := snapshot.nodeInfoMap[nodeName]; n != nil { if n := snapshot.nodeInfoMap[nodeName]; n != nil {
snapshot.nodeInfoList = append(snapshot.nodeInfoList, n) snapshot.nodeInfoList = append(snapshot.nodeInfoList, n)
if len(n.PodsWithAffinity) > 0 { if len(n.PodsWithAffinity) > 0 {

View File

@ -1090,7 +1090,11 @@ func TestNodeOperators(t *testing.T) {
if !found { if !found {
t.Errorf("Failed to find node %v in internalcache.", node.Name) t.Errorf("Failed to find node %v in internalcache.", node.Name)
} }
if cache.nodeTree.numNodes != 1 || cache.nodeTree.next() != node.Name { nodesList, err := cache.nodeTree.list()
if err != nil {
t.Fatal(err)
}
if cache.nodeTree.numNodes != 1 || nodesList[len(nodesList)-1] != node.Name {
t.Errorf("cache.nodeTree is not updated correctly after adding node: %v", node.Name) t.Errorf("cache.nodeTree is not updated correctly after adding node: %v", node.Name)
} }
@ -1134,7 +1138,11 @@ func TestNodeOperators(t *testing.T) {
t.Errorf("Failed to update node in schedulertypes:\n got: %+v \nexpected: %+v", got, expected) t.Errorf("Failed to update node in schedulertypes:\n got: %+v \nexpected: %+v", got, expected)
} }
// Check nodeTree after update // Check nodeTree after update
if cache.nodeTree.numNodes != 1 || cache.nodeTree.next() != node.Name { nodesList, err = cache.nodeTree.list()
if err != nil {
t.Fatal(err)
}
if cache.nodeTree.numNodes != 1 || nodesList[len(nodesList)-1] != node.Name {
t.Errorf("unexpected cache.nodeTree after updating node: %v", node.Name) t.Errorf("unexpected cache.nodeTree after updating node: %v", node.Name)
} }
@ -1147,8 +1155,12 @@ func TestNodeOperators(t *testing.T) {
} else if n != nil { } else if n != nil {
t.Errorf("The node object for %v should be nil", node.Name) t.Errorf("The node object for %v should be nil", node.Name)
} }
// Check node is removed from nodeTree. // Check node is removed from nodeTree as well.
if cache.nodeTree.numNodes != 0 || cache.nodeTree.next() != "" { nodesList, err = cache.nodeTree.list()
if err != nil {
t.Fatal(err)
}
if cache.nodeTree.numNodes != 0 || len(nodesList) != 0 {
t.Errorf("unexpected cache.nodeTree after removing node: %v", node.Name) t.Errorf("unexpected cache.nodeTree after removing node: %v", node.Name)
} }
// Pods are still in the pods cache. // Pods are still in the pods cache.
@ -1306,7 +1318,7 @@ func TestSchedulerCache_UpdateSnapshot(t *testing.T) {
updateSnapshot := func() operation { updateSnapshot := func() operation {
return func() { return func() {
cache.UpdateSnapshot(snapshot) cache.UpdateSnapshot(snapshot)
if err := compareCacheWithNodeInfoSnapshot(cache, snapshot); err != nil { if err := compareCacheWithNodeInfoSnapshot(t, cache, snapshot); err != nil {
t.Error(err) t.Error(err)
} }
} }
@ -1487,14 +1499,14 @@ func TestSchedulerCache_UpdateSnapshot(t *testing.T) {
if err := cache.UpdateSnapshot(snapshot); err != nil { if err := cache.UpdateSnapshot(snapshot); err != nil {
t.Error(err) t.Error(err)
} }
if err := compareCacheWithNodeInfoSnapshot(cache, snapshot); err != nil { if err := compareCacheWithNodeInfoSnapshot(t, cache, snapshot); err != nil {
t.Error(err) t.Error(err)
} }
}) })
} }
} }
func compareCacheWithNodeInfoSnapshot(cache *schedulerCache, snapshot *Snapshot) error { func compareCacheWithNodeInfoSnapshot(t *testing.T, cache *schedulerCache, snapshot *Snapshot) error {
// Compare the map. // Compare the map.
if len(snapshot.nodeInfoMap) != len(cache.nodes) { if len(snapshot.nodeInfoMap) != len(cache.nodes) {
return fmt.Errorf("unexpected number of nodes in the snapshot. Expected: %v, got: %v", len(cache.nodes), len(snapshot.nodeInfoMap)) return fmt.Errorf("unexpected number of nodes in the snapshot. Expected: %v, got: %v", len(cache.nodes), len(snapshot.nodeInfoMap))
@ -1512,8 +1524,11 @@ func compareCacheWithNodeInfoSnapshot(cache *schedulerCache, snapshot *Snapshot)
expectedNodeInfoList := make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes) expectedNodeInfoList := make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes)
expectedHavePodsWithAffinityNodeInfoList := make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes) expectedHavePodsWithAffinityNodeInfoList := make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes)
for i := 0; i < cache.nodeTree.numNodes; i++ { nodesList, err := cache.nodeTree.list()
nodeName := cache.nodeTree.next() if err != nil {
t.Fatal(err)
}
for _, nodeName := range nodesList {
if n := snapshot.nodeInfoMap[nodeName]; n != nil { if n := snapshot.nodeInfoMap[nodeName]; n != nil {
expectedNodeInfoList = append(expectedNodeInfoList, n) expectedNodeInfoList = append(expectedNodeInfoList, n)
if len(n.PodsWithAffinity) > 0 { if len(n.PodsWithAffinity) > 0 {
@ -1576,7 +1591,7 @@ func TestSchedulerCache_updateNodeInfoSnapshotList(t *testing.T) {
updateSnapshot := func(t *testing.T) { updateSnapshot := func(t *testing.T) {
cache.updateNodeInfoSnapshotList(snapshot, true) cache.updateNodeInfoSnapshotList(snapshot, true)
if err := compareCacheWithNodeInfoSnapshot(cache, snapshot); err != nil { if err := compareCacheWithNodeInfoSnapshot(t, cache, snapshot); err != nil {
t.Error(err) t.Error(err)
} }
} }
@ -1672,7 +1687,7 @@ func TestSchedulerCache_updateNodeInfoSnapshotList(t *testing.T) {
// Always update the snapshot at the end of operations and compare it. // Always update the snapshot at the end of operations and compare it.
cache.updateNodeInfoSnapshotList(snapshot, true) cache.updateNodeInfoSnapshotList(snapshot, true)
if err := compareCacheWithNodeInfoSnapshot(cache, snapshot); err != nil { if err := compareCacheWithNodeInfoSnapshot(t, cache, snapshot); err != nil {
t.Error(err) t.Error(err)
} }
nodeNames := make([]string, len(snapshot.nodeInfoList)) nodeNames := make([]string, len(snapshot.nodeInfoList))

View File

@ -17,9 +17,10 @@ limitations under the License.
package cache package cache
import ( import (
"errors"
"fmt" "fmt"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/klog/v2" "k8s.io/klog/v2"
utilnode "k8s.io/kubernetes/pkg/util/node" utilnode "k8s.io/kubernetes/pkg/util/node"
) )
@ -29,37 +30,15 @@ import (
// NodeTree is NOT thread-safe, any concurrent updates/reads from it must be synchronized by the caller. // NodeTree is NOT thread-safe, any concurrent updates/reads from it must be synchronized by the caller.
// It is used only by schedulerCache, and should stay as such. // It is used only by schedulerCache, and should stay as such.
type nodeTree struct { type nodeTree struct {
tree map[string]*nodeArray // a map from zone (region-zone) to an array of nodes in the zone. tree map[string][]string // a map from zone (region-zone) to an array of nodes in the zone.
zones []string // a list of all the zones in the tree (keys) zones []string // a list of all the zones in the tree (keys)
zoneIndex int numNodes int
numNodes int
}
// nodeArray is a struct that has nodes that are in a zone.
// We use a slice (as opposed to a set/map) to store the nodes because iterating over the nodes is
// a lot more frequent than searching them by name.
type nodeArray struct {
nodes []string
lastIndex int
}
func (na *nodeArray) next() (nodeName string, exhausted bool) {
if len(na.nodes) == 0 {
klog.Error("The nodeArray is empty. It should have been deleted from NodeTree.")
return "", false
}
if na.lastIndex >= len(na.nodes) {
return "", true
}
nodeName = na.nodes[na.lastIndex]
na.lastIndex++
return nodeName, false
} }
// newNodeTree creates a NodeTree from nodes. // newNodeTree creates a NodeTree from nodes.
func newNodeTree(nodes []*v1.Node) *nodeTree { func newNodeTree(nodes []*v1.Node) *nodeTree {
nt := &nodeTree{ nt := &nodeTree{
tree: make(map[string]*nodeArray), tree: make(map[string][]string),
} }
for _, n := range nodes { for _, n := range nodes {
nt.addNode(n) nt.addNode(n)
@ -72,16 +51,16 @@ func newNodeTree(nodes []*v1.Node) *nodeTree {
func (nt *nodeTree) addNode(n *v1.Node) { func (nt *nodeTree) addNode(n *v1.Node) {
zone := utilnode.GetZoneKey(n) zone := utilnode.GetZoneKey(n)
if na, ok := nt.tree[zone]; ok { if na, ok := nt.tree[zone]; ok {
for _, nodeName := range na.nodes { for _, nodeName := range na {
if nodeName == n.Name { if nodeName == n.Name {
klog.Warningf("node %q already exist in the NodeTree", n.Name) klog.Warningf("node %q already exist in the NodeTree", n.Name)
return return
} }
} }
na.nodes = append(na.nodes, n.Name) nt.tree[zone] = append(na, n.Name)
} else { } else {
nt.zones = append(nt.zones, zone) nt.zones = append(nt.zones, zone)
nt.tree[zone] = &nodeArray{nodes: []string{n.Name}, lastIndex: 0} nt.tree[zone] = []string{n.Name}
} }
klog.V(2).Infof("Added node %q in group %q to NodeTree", n.Name, zone) klog.V(2).Infof("Added node %q in group %q to NodeTree", n.Name, zone)
nt.numNodes++ nt.numNodes++
@ -91,10 +70,10 @@ func (nt *nodeTree) addNode(n *v1.Node) {
func (nt *nodeTree) removeNode(n *v1.Node) error { func (nt *nodeTree) removeNode(n *v1.Node) error {
zone := utilnode.GetZoneKey(n) zone := utilnode.GetZoneKey(n)
if na, ok := nt.tree[zone]; ok { if na, ok := nt.tree[zone]; ok {
for i, nodeName := range na.nodes { for i, nodeName := range na {
if nodeName == n.Name { if nodeName == n.Name {
na.nodes = append(na.nodes[:i], na.nodes[i+1:]...) nt.tree[zone] = append(na[:i], na[i+1:]...)
if len(na.nodes) == 0 { if len(nt.tree[zone]) == 0 {
nt.removeZone(zone) nt.removeZone(zone)
} }
klog.V(2).Infof("Removed node %q in group %q from NodeTree", n.Name, zone) klog.V(2).Infof("Removed node %q in group %q from NodeTree", n.Name, zone)
@ -135,36 +114,30 @@ func (nt *nodeTree) updateNode(old, new *v1.Node) {
nt.addNode(new) nt.addNode(new)
} }
func (nt *nodeTree) resetExhausted() { // list returns the list of names of the node. NodeTree iterates over zones and in each zone iterates
for _, na := range nt.tree {
na.lastIndex = 0
}
nt.zoneIndex = 0
}
// next returns the name of the next node. NodeTree iterates over zones and in each zone iterates
// over nodes in a round robin fashion. // over nodes in a round robin fashion.
func (nt *nodeTree) next() string { func (nt *nodeTree) list() ([]string, error) {
if len(nt.zones) == 0 { if len(nt.zones) == 0 {
return "" return nil, nil
} }
nodesList := make([]string, 0, nt.numNodes)
numExhaustedZones := 0 numExhaustedZones := 0
for { nodeIndex := 0
if nt.zoneIndex >= len(nt.zones) { for len(nodesList) < nt.numNodes {
nt.zoneIndex = 0 if numExhaustedZones >= len(nt.zones) { // all zones are exhausted.
return nodesList, errors.New("all zones exhausted before reaching count of nodes expected")
} }
zone := nt.zones[nt.zoneIndex] for zoneIndex := 0; zoneIndex < len(nt.zones); zoneIndex++ {
nt.zoneIndex++ na := nt.tree[nt.zones[zoneIndex]]
// We do not check the exhausted zones before calling next() on the zone. This ensures if nodeIndex >= len(na) { // If the zone is exhausted, continue
// that if more nodes are added to a zone after it is exhausted, we iterate over the new nodes. if nodeIndex == len(na) { // If it is the first time the zone is exhausted
nodeName, exhausted := nt.tree[zone].next() numExhaustedZones++
if exhausted { }
numExhaustedZones++ continue
if numExhaustedZones >= len(nt.zones) { // all zones are exhausted. we should reset.
nt.resetExhausted()
} }
} else { nodesList = append(nodesList, na[nodeIndex])
return nodeName
} }
nodeIndex++
} }
return nodesList, nil
} }

View File

@ -20,7 +20,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -133,10 +133,10 @@ var allNodes = []*v1.Node{
}, },
} }
func verifyNodeTree(t *testing.T, nt *nodeTree, expectedTree map[string]*nodeArray) { func verifyNodeTree(t *testing.T, nt *nodeTree, expectedTree map[string][]string) {
expectedNumNodes := int(0) expectedNumNodes := int(0)
for _, na := range expectedTree { for _, na := range expectedTree {
expectedNumNodes += len(na.nodes) expectedNumNodes += len(na)
} }
if numNodes := nt.numNodes; numNodes != expectedNumNodes { if numNodes := nt.numNodes; numNodes != expectedNumNodes {
t.Errorf("unexpected nodeTree.numNodes. Expected: %v, Got: %v", expectedNumNodes, numNodes) t.Errorf("unexpected nodeTree.numNodes. Expected: %v, Got: %v", expectedNumNodes, numNodes)
@ -158,41 +158,41 @@ func TestNodeTree_AddNode(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
nodesToAdd []*v1.Node nodesToAdd []*v1.Node
expectedTree map[string]*nodeArray expectedTree map[string][]string
}{ }{
{ {
name: "single node no labels", name: "single node no labels",
nodesToAdd: allNodes[:1], nodesToAdd: allNodes[:1],
expectedTree: map[string]*nodeArray{"": {[]string{"node-0"}, 0}}, expectedTree: map[string][]string{"": {"node-0"}},
}, },
{ {
name: "mix of nodes with and without proper labels", name: "mix of nodes with and without proper labels",
nodesToAdd: allNodes[:4], nodesToAdd: allNodes[:4],
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"": {[]string{"node-0"}, 0}, "": {"node-0"},
"region-1:\x00:": {[]string{"node-1"}, 0}, "region-1:\x00:": {"node-1"},
":\x00:zone-2": {[]string{"node-2"}, 0}, ":\x00:zone-2": {"node-2"},
"region-1:\x00:zone-2": {[]string{"node-3"}, 0}, "region-1:\x00:zone-2": {"node-3"},
}, },
}, },
{ {
name: "mix of nodes with and without proper labels and some zones with multiple nodes", name: "mix of nodes with and without proper labels and some zones with multiple nodes",
nodesToAdd: allNodes[:7], nodesToAdd: allNodes[:7],
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"": {[]string{"node-0"}, 0}, "": {"node-0"},
"region-1:\x00:": {[]string{"node-1"}, 0}, "region-1:\x00:": {"node-1"},
":\x00:zone-2": {[]string{"node-2"}, 0}, ":\x00:zone-2": {"node-2"},
"region-1:\x00:zone-2": {[]string{"node-3", "node-4"}, 0}, "region-1:\x00:zone-2": {"node-3", "node-4"},
"region-1:\x00:zone-3": {[]string{"node-5"}, 0}, "region-1:\x00:zone-3": {"node-5"},
"region-2:\x00:zone-2": {[]string{"node-6"}, 0}, "region-2:\x00:zone-2": {"node-6"},
}, },
}, },
{ {
name: "nodes also using deprecated zone/region label", name: "nodes also using deprecated zone/region label",
nodesToAdd: allNodes[9:], nodesToAdd: allNodes[9:],
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"region-2:\x00:zone-2": {[]string{"node-9"}, 0}, "region-2:\x00:zone-2": {"node-9"},
"region-2:\x00:zone-3": {[]string{"node-10"}, 0}, "region-2:\x00:zone-3": {"node-10"},
}, },
}, },
} }
@ -213,43 +213,43 @@ func TestNodeTree_RemoveNode(t *testing.T) {
name string name string
existingNodes []*v1.Node existingNodes []*v1.Node
nodesToRemove []*v1.Node nodesToRemove []*v1.Node
expectedTree map[string]*nodeArray expectedTree map[string][]string
expectError bool expectError bool
}{ }{
{ {
name: "remove a single node with no labels", name: "remove a single node with no labels",
existingNodes: allNodes[:7], existingNodes: allNodes[:7],
nodesToRemove: allNodes[:1], nodesToRemove: allNodes[:1],
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"region-1:\x00:": {[]string{"node-1"}, 0}, "region-1:\x00:": {"node-1"},
":\x00:zone-2": {[]string{"node-2"}, 0}, ":\x00:zone-2": {"node-2"},
"region-1:\x00:zone-2": {[]string{"node-3", "node-4"}, 0}, "region-1:\x00:zone-2": {"node-3", "node-4"},
"region-1:\x00:zone-3": {[]string{"node-5"}, 0}, "region-1:\x00:zone-3": {"node-5"},
"region-2:\x00:zone-2": {[]string{"node-6"}, 0}, "region-2:\x00:zone-2": {"node-6"},
}, },
}, },
{ {
name: "remove a few nodes including one from a zone with multiple nodes", name: "remove a few nodes including one from a zone with multiple nodes",
existingNodes: allNodes[:7], existingNodes: allNodes[:7],
nodesToRemove: allNodes[1:4], nodesToRemove: allNodes[1:4],
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"": {[]string{"node-0"}, 0}, "": {"node-0"},
"region-1:\x00:zone-2": {[]string{"node-4"}, 0}, "region-1:\x00:zone-2": {"node-4"},
"region-1:\x00:zone-3": {[]string{"node-5"}, 0}, "region-1:\x00:zone-3": {"node-5"},
"region-2:\x00:zone-2": {[]string{"node-6"}, 0}, "region-2:\x00:zone-2": {"node-6"},
}, },
}, },
{ {
name: "remove all nodes", name: "remove all nodes",
existingNodes: allNodes[:7], existingNodes: allNodes[:7],
nodesToRemove: allNodes[:7], nodesToRemove: allNodes[:7],
expectedTree: map[string]*nodeArray{}, expectedTree: map[string][]string{},
}, },
{ {
name: "remove non-existing node", name: "remove non-existing node",
existingNodes: nil, existingNodes: nil,
nodesToRemove: allNodes[:5], nodesToRemove: allNodes[:5],
expectedTree: map[string]*nodeArray{}, expectedTree: map[string][]string{},
expectError: true, expectError: true,
}, },
} }
@ -273,7 +273,7 @@ func TestNodeTree_UpdateNode(t *testing.T) {
name string name string
existingNodes []*v1.Node existingNodes []*v1.Node
nodeToUpdate *v1.Node nodeToUpdate *v1.Node
expectedTree map[string]*nodeArray expectedTree map[string][]string
}{ }{
{ {
name: "update a node without label", name: "update a node without label",
@ -287,12 +287,12 @@ func TestNodeTree_UpdateNode(t *testing.T) {
}, },
}, },
}, },
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"region-1:\x00:": {[]string{"node-1"}, 0}, "region-1:\x00:": {"node-1"},
":\x00:zone-2": {[]string{"node-2"}, 0}, ":\x00:zone-2": {"node-2"},
"region-1:\x00:zone-2": {[]string{"node-3", "node-4", "node-0"}, 0}, "region-1:\x00:zone-2": {"node-3", "node-4", "node-0"},
"region-1:\x00:zone-3": {[]string{"node-5"}, 0}, "region-1:\x00:zone-3": {"node-5"},
"region-2:\x00:zone-2": {[]string{"node-6"}, 0}, "region-2:\x00:zone-2": {"node-6"},
}, },
}, },
{ {
@ -307,8 +307,8 @@ func TestNodeTree_UpdateNode(t *testing.T) {
}, },
}, },
}, },
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"region-1:\x00:zone-2": {[]string{"node-0"}, 0}, "region-1:\x00:zone-2": {"node-0"},
}, },
}, },
{ {
@ -323,9 +323,9 @@ func TestNodeTree_UpdateNode(t *testing.T) {
}, },
}, },
}, },
expectedTree: map[string]*nodeArray{ expectedTree: map[string][]string{
"": {[]string{"node-0"}, 0}, "": {"node-0"},
"region-1:\x00:zone-2": {[]string{"node-new"}, 0}, "region-1:\x00:zone-2": {"node-new"},
}, },
}, },
} }
@ -349,36 +349,31 @@ func TestNodeTree_UpdateNode(t *testing.T) {
} }
} }
func TestNodeTree_Next(t *testing.T) { func TestNodeTree_List(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
nodesToAdd []*v1.Node nodesToAdd []*v1.Node
numRuns int // number of times to run Next()
expectedOutput []string expectedOutput []string
}{ }{
{ {
name: "empty tree", name: "empty tree",
nodesToAdd: nil, nodesToAdd: nil,
numRuns: 2, expectedOutput: nil,
expectedOutput: []string{"", ""},
}, },
{ {
name: "should go back to the first node after finishing a round", name: "one node",
nodesToAdd: allNodes[:1], nodesToAdd: allNodes[:1],
numRuns: 2, expectedOutput: []string{"node-0"},
expectedOutput: []string{"node-0", "node-0"},
}, },
{ {
name: "should go back to the first node after going over all nodes", name: "four nodes",
nodesToAdd: allNodes[:4], nodesToAdd: allNodes[:4],
numRuns: 5, expectedOutput: []string{"node-0", "node-1", "node-2", "node-3"},
expectedOutput: []string{"node-0", "node-1", "node-2", "node-3", "node-0"},
}, },
{ {
name: "should go to all zones before going to the second nodes in the same zone", name: "all nodes",
nodesToAdd: allNodes[:9], nodesToAdd: allNodes[:9],
numRuns: 11, expectedOutput: []string{"node-0", "node-1", "node-2", "node-3", "node-5", "node-6", "node-4", "node-7", "node-8"},
expectedOutput: []string{"node-0", "node-1", "node-2", "node-3", "node-5", "node-6", "node-4", "node-7", "node-8", "node-0", "node-1"},
}, },
} }
@ -386,9 +381,9 @@ func TestNodeTree_Next(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
nt := newNodeTree(test.nodesToAdd) nt := newNodeTree(test.nodesToAdd)
var output []string output, err := nt.list()
for i := 0; i < test.numRuns; i++ { if err != nil {
output = append(output, nt.next()) t.Fatal(err)
} }
if !reflect.DeepEqual(output, test.expectedOutput) { if !reflect.DeepEqual(output, test.expectedOutput) {
t.Errorf("unexpected output. Expected: %v, Got: %v", test.expectedOutput, output) t.Errorf("unexpected output. Expected: %v, Got: %v", test.expectedOutput, output)
@ -397,6 +392,15 @@ func TestNodeTree_Next(t *testing.T) {
} }
} }
func TestNodeTree_List_Exhausted(t *testing.T) {
nt := newNodeTree(allNodes[:9])
nt.numNodes++
_, err := nt.list()
if err == nil {
t.Fatal("Expected an error from zone exhaustion")
}
}
func TestNodeTreeMultiOperations(t *testing.T) { func TestNodeTreeMultiOperations(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -406,39 +410,39 @@ func TestNodeTreeMultiOperations(t *testing.T) {
expectedOutput []string expectedOutput []string
}{ }{
{ {
name: "add and remove all nodes between two Next operations", name: "add and remove all nodes",
nodesToAdd: allNodes[2:9], nodesToAdd: allNodes[2:9],
nodesToRemove: allNodes[2:9], nodesToRemove: allNodes[2:9],
operations: []string{"add", "add", "next", "add", "remove", "remove", "remove", "next"}, operations: []string{"add", "add", "add", "remove", "remove", "remove"},
expectedOutput: []string{"node-2", ""}, expectedOutput: nil,
}, },
{ {
name: "add and remove some nodes between two Next operations", name: "add and remove some nodes",
nodesToAdd: allNodes[2:9], nodesToAdd: allNodes[2:9],
nodesToRemove: allNodes[2:9], nodesToRemove: allNodes[2:9],
operations: []string{"add", "add", "next", "add", "remove", "remove", "next"}, operations: []string{"add", "add", "add", "remove"},
expectedOutput: []string{"node-2", "node-4"}, expectedOutput: []string{"node-3", "node-4"},
}, },
{ {
name: "remove nodes already iterated on and add new nodes", name: "remove three nodes",
nodesToAdd: allNodes[2:9], nodesToAdd: allNodes[2:9],
nodesToRemove: allNodes[2:9], nodesToRemove: allNodes[2:9],
operations: []string{"add", "add", "next", "next", "add", "remove", "remove", "next"}, operations: []string{"add", "add", "add", "remove", "remove", "remove", "add"},
expectedOutput: []string{"node-2", "node-3", "node-4"}, expectedOutput: []string{"node-5"},
}, },
{ {
name: "add more nodes to an exhausted zone", name: "add more nodes to an exhausted zone",
nodesToAdd: append(allNodes[4:9:9], allNodes[3]), nodesToAdd: append(allNodes[4:9:9], allNodes[3]),
nodesToRemove: nil, nodesToRemove: nil,
operations: []string{"add", "add", "add", "add", "add", "next", "next", "next", "next", "add", "next", "next", "next"}, operations: []string{"add", "add", "add", "add", "add", "add"},
expectedOutput: []string{"node-4", "node-5", "node-6", "node-7", "node-3", "node-8", "node-4"}, expectedOutput: []string{"node-4", "node-5", "node-6", "node-3", "node-7", "node-8"},
}, },
{ {
name: "remove zone and add new to ensure exhausted is reset correctly", name: "remove zone and add new",
nodesToAdd: append(allNodes[3:5:5], allNodes[6:8]...), nodesToAdd: append(allNodes[3:5:5], allNodes[6:8]...),
nodesToRemove: allNodes[3:5], nodesToRemove: allNodes[3:5],
operations: []string{"add", "add", "next", "next", "remove", "add", "add", "next", "next", "remove", "next", "next"}, operations: []string{"add", "add", "remove", "add", "add", "remove"},
expectedOutput: []string{"node-3", "node-4", "node-6", "node-7", "node-6", "node-7"}, expectedOutput: []string{"node-6", "node-7"},
}, },
} }
@ -447,7 +451,6 @@ func TestNodeTreeMultiOperations(t *testing.T) {
nt := newNodeTree(nil) nt := newNodeTree(nil)
addIndex := 0 addIndex := 0
removeIndex := 0 removeIndex := 0
var output []string
for _, op := range test.operations { for _, op := range test.operations {
switch op { switch op {
case "add": case "add":
@ -464,12 +467,14 @@ func TestNodeTreeMultiOperations(t *testing.T) {
nt.removeNode(test.nodesToRemove[removeIndex]) nt.removeNode(test.nodesToRemove[removeIndex])
removeIndex++ removeIndex++
} }
case "next":
output = append(output, nt.next())
default: default:
t.Errorf("unknow operation: %v", op) t.Errorf("unknow operation: %v", op)
} }
} }
output, err := nt.list()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(output, test.expectedOutput) { if !reflect.DeepEqual(output, test.expectedOutput) {
t.Errorf("unexpected output. Expected: %v, Got: %v", test.expectedOutput, output) t.Errorf("unexpected output. Expected: %v, Got: %v", test.expectedOutput, output)
} }