Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 180 additions & 54 deletions pkg/gameserverallocations/allocation_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"sort"

"agones.dev/agones/pkg/apis"
"agones.dev/agones/pkg/apis/agones"
agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
Expand Down Expand Up @@ -151,6 +152,23 @@ func (c *AllocationCache) AddGameServer(gs *agonesv1.GameServer) {
c.cache.Store(key, gs)
}

// refreshGameServer updates the cache with the live version of the GameServer read from the API server:
// it is stored if it is still allocatable, and removed from the cache otherwise
func (c *AllocationCache) refreshGameServer(gs *agonesv1.GameServer) {
key, _ := cache.MetaNamespaceKeyFunc(gs)

if !gs.IsBeingDeleted() && c.matcher(gs) {
c.cache.Store(key, gs)
return
}
c.cache.Delete(key)
}

// GetGameServer returns the cached GameServer for the given key, along with a boolean indicating if it was found
func (c *AllocationCache) GetGameServer(key string) (*agonesv1.GameServer, bool) {
return c.cache.Load(key)
}

// getGameServers returns a list of game servers in the cache.
func (c *AllocationCache) getGameServers() []*agonesv1.GameServer {
length := c.cache.Len()
Expand All @@ -175,50 +193,12 @@ func (c *AllocationCache) ListSortedGameServers(gsa *allocationv1.GameServerAllo
}
counts := c.counter.Counts()

var priorities []agonesv1.Priority
if gsa != nil {
priorities = gsa.Spec.Priorities
}
sort.Slice(list, func(i, j int) bool {
gs1 := list[i]
gs2 := list[j]

// Search Allocated GameServers first.
if gs1.Status.State != gs2.Status.State {
return gs1.Status.State == agonesv1.GameServerStateAllocated
}

c1, ok := counts[gs1.Status.NodeName]
if !ok {
return false
}

c2, ok := counts[gs2.Status.NodeName]
if !ok {
return true
}

if c1.Allocated > c2.Allocated {
return true
}
if c1.Allocated < c2.Allocated {
return false
}

// prefer nodes that have the most Ready gameservers on them - they are most likely to be
// completely filled and least likely target for scale down.
if c1.Ready < c2.Ready {
return false
}
if c1.Ready > c2.Ready {
return true
}

// if we end up here, then break the tie with Counter or List Priority.
if gsa != nil {
if res := gs1.CompareCountAndListPriorities(gsa.Spec.Priorities, gs2); res != nil {
return *res
}
}

// finally sort lexicographically, so we have a stable order
return gs1.GetObjectMeta().GetName() < gs2.GetObjectMeta().GetName()
return compareGameServersForPackedStrategy(list[i], list[j], priorities, counts)
})

return list
Expand All @@ -232,18 +212,12 @@ func (c *AllocationCache) ListSortedGameServersPriorities(gsa *allocationv1.Game
return []*agonesv1.GameServer{}
}

var priorities []agonesv1.Priority
if gsa != nil {
priorities = gsa.Spec.Priorities
}
sort.Slice(list, func(i, j int) bool {
gs1 := list[i]
gs2 := list[j]

if gsa != nil {
if res := gs1.CompareCountAndListPriorities(gsa.Spec.Priorities, gs2); res != nil {
return *res
}
}

// finally sort lexicographically, so we have a stable order
return gs1.GetObjectMeta().GetName() < gs2.GetObjectMeta().GetName()
return compareGameServersForDistributedStrategy(list[i], list[j], priorities)
})

return list
Expand Down Expand Up @@ -314,3 +288,155 @@ func (c *AllocationCache) getKey(gs *agonesv1.GameServer) (string, bool) {
}
return key, ok
}

// ReorderGameServerAfterAllocation repositions gsAfterAllocation in gsList after a local allocation
// has been applied, using gsIndexBeforeAllocation as a search hint. Used by the batch allocator
// to maintain sort order without a full re-sort.
func (c *AllocationCache) ReorderGameServerAfterAllocation(
gsList []*agonesv1.GameServer,
gsIndexBeforeAllocation int, gsAfterAllocation *agonesv1.GameServer,
priorities []agonesv1.Priority, strategy apis.SchedulingStrategy) {
if len(gsList) == 0 || gsIndexBeforeAllocation < 0 || gsIndexBeforeAllocation >= len(gsList) ||
gsList[gsIndexBeforeAllocation] == nil || gsAfterAllocation == nil {
c.baseLogger.WithField("gsIndexBeforeAllocation", gsIndexBeforeAllocation).
WithField("gsAfterAllocation", gsAfterAllocation).
WithField("gsListLength", len(gsList)).
Warn("ReorderGameServerAfterAllocation called with invalid parameters, reordering is skipped")
return
}

newIndex := gsIndexBeforeAllocation
gsToReorderOriginal := gsList[gsIndexBeforeAllocation]

optimizeList := func(greater bool) []*agonesv1.GameServer {
if greater {
return gsList[gsIndexBeforeAllocation+1:]
}
return gsList[:gsIndexBeforeAllocation]
}

switch strategy {
case apis.Packed:
counts := c.counter.Counts()
greater, equal := compareGameServersAfterAllocationForPackedStrategy(gsToReorderOriginal, gsAfterAllocation, priorities, counts)
if !equal {
newIndex = findIndexAfterAllocationForPackedStrategy(optimizeList(greater), gsAfterAllocation, priorities, counts)
if greater {
newIndex += gsIndexBeforeAllocation
}
}
case apis.Distributed:
greater, equal := compareGameServersAfterAllocationForDistributedStrategy(gsToReorderOriginal, gsAfterAllocation, priorities)
if !equal {
newIndex = findIndexAfterAllocationForDistributedStrategy(optimizeList(greater), gsAfterAllocation, priorities)
if greater {
newIndex += gsIndexBeforeAllocation
}
}
default:
c.baseLogger.WithField("strategy", strategy).
Warn("scheduling strategy not supported, reordering is skipped")
}

if newIndex != gsIndexBeforeAllocation {
gsList = append(gsList[:gsIndexBeforeAllocation], gsList[gsIndexBeforeAllocation+1:]...)
gsList = append(gsList[:newIndex], append([]*agonesv1.GameServer{gsAfterAllocation}, gsList[newIndex:]...)...)
} else {
gsList[gsIndexBeforeAllocation] = gsAfterAllocation
}
}

// compareGameServersAfterAllocationForPackedStrategy compares a game server before and after a local
// allocation is applied. Returns (greater, equal) where greater means before has higher priority than after.
func compareGameServersAfterAllocationForPackedStrategy(
before, after *agonesv1.GameServer,
priorities []agonesv1.Priority,
counts map[string]gameservers.NodeCount) (bool, bool) {
if before.Status.State != after.Status.State {
return before.Status.State == agonesv1.GameServerStateAllocated, false
}

c1, ok := counts[before.Status.NodeName]
if !ok {
return false, false
}
c2, ok := counts[after.Status.NodeName]
if !ok {
return true, false
}

if c1.Allocated > c2.Allocated {
return true, false
}
if c1.Allocated < c2.Allocated {
return false, false
}

if c1.Ready < c2.Ready {
return false, false
}
if c1.Ready > c2.Ready {
return true, false
}

if priorities != nil {
if res := before.CompareCountAndListPriorities(priorities, after); res != nil {
return *res, false
}
}

return false, true
}

// compareGameServersAfterAllocationForDistributedStrategy compares a game server before and after a local
// allocation is applied using the distributed strategy. Returns (greater, equal).
func compareGameServersAfterAllocationForDistributedStrategy(
before, after *agonesv1.GameServer,
priorities []agonesv1.Priority) (bool, bool) {
if priorities != nil {
if res := before.CompareCountAndListPriorities(priorities, after); res != nil {
return *res, false
}
}
return false, true
}

// compareGameServersForPackedStrategy compares two game servers for the packed strategy with
// lexicographic tie-breaking for a stable sort order.
func compareGameServersForPackedStrategy(gs0, gs1 *agonesv1.GameServer, priorities []agonesv1.Priority, counts map[string]gameservers.NodeCount) bool {
if gs1 == nil {
return true
}
greater, equal := compareGameServersAfterAllocationForPackedStrategy(gs0, gs1, priorities, counts)
if !equal {
return greater
}
return gs0.GetObjectMeta().GetName() < gs1.GetObjectMeta().GetName()
}

// compareGameServersForDistributedStrategy compares two game servers for the distributed strategy with
// lexicographic tie-breaking for a stable sort order.
func compareGameServersForDistributedStrategy(gs0, gs1 *agonesv1.GameServer, priorities []agonesv1.Priority) bool {
if gs1 == nil {
return true
}
greater, equal := compareGameServersAfterAllocationForDistributedStrategy(gs0, gs1, priorities)
if !equal {
return greater
}
return gs0.GetObjectMeta().GetName() < gs1.GetObjectMeta().GetName()
}

// findIndexAfterAllocationForPackedStrategy binary-searches for the insertion position that maintains sort order.
func findIndexAfterAllocationForPackedStrategy(gsList []*agonesv1.GameServer, gs *agonesv1.GameServer, priorities []agonesv1.Priority, counts map[string]gameservers.NodeCount) int {
return sort.Search(len(gsList), func(i int) bool {
return compareGameServersForPackedStrategy(gs, gsList[i], priorities, counts)
})
}

// findIndexAfterAllocationForDistributedStrategy binary-searches for the insertion position that maintains sort order.
func findIndexAfterAllocationForDistributedStrategy(gsList []*agonesv1.GameServer, gs *agonesv1.GameServer, priorities []agonesv1.Priority) int {
return sort.Search(len(gsList), func(i int) bool {
return compareGameServersForDistributedStrategy(gs, gsList[i], priorities)
})
}
Loading
Loading