Skip to content

Commit

Permalink
Sort By Counters or Lists during GameServerAllocation 2716 (#3091)
Browse files Browse the repository at this point in the history
* GameServerAllocation sorting based on CountsAndLists Priorities

* Updates to follow existing patterns for Packed and Distributed

* Generated Files

* Adds additional tests for CountsAndLists validation
  • Loading branch information
igooch committed Apr 20, 2023
1 parent e61be47 commit 5911f0c
Show file tree
Hide file tree
Showing 8 changed files with 2,256 additions and 1,450 deletions.
66 changes: 66 additions & 0 deletions pkg/apis/allocation/v1/gameserverallocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ const (
// GameServerAllocationContention when the allocation is unsuccessful
// because of contention
GameServerAllocationContention GameServerAllocationState = "Contention"
// GameServerAllocationPriorityCounter is a PriorityType for sorting Game Servers by Counter
GameServerAllocationPriorityCounter string = "Counter"
// GameServerAllocationPriorityList is a PriorityType for sorting Game Servers by List
GameServerAllocationPriorityList string = "List"
// GameServerAllocationAscending is a Priority Order where the smaller count is preferred in sorting.
GameServerAllocationAscending string = "Ascending"
// GameServerAllocationDescending is a Priority Order where the larger count is preferred in sorting.
GameServerAllocationDescending string = "Descending"
)

// GameServerAllocationState is the Allocation state
Expand Down Expand Up @@ -82,6 +90,13 @@ type GameServerAllocationSpec struct {
// This is useful for things like smoke testing of new game servers.
Preferred []GameServerSelector `json:"preferred,omitempty"`

// (Alpha, CountsAndLists feature flag) The first Priority on the array of Priorities is the most
// important for sorting. The allocator will use the first priority for sorting GameServers in the
// Selector set, and will only use any following priority for tie-breaking during sort.
// Impacts which GameServer is checked first.
// +optional
Priorities []Priority `json:"priorities,omitempty"`

// Ordered list of GameServer label selectors.
// If the first selector is not matched, the selection attempts the second selector, and so on.
// This is useful for things like smoke testing of new game servers.
Expand Down Expand Up @@ -484,6 +499,41 @@ func (mp *MetaPatch) Validate() ([]metav1.StatusCause, bool) {
return causes, len(causes) == 0
}

// Priority is a sorting option for GameServers with Counters or Lists based on the count or
// number of items in a List.
// PriorityType: Sort by a "Counter" or a "List".
// Key: The name of the Counter or List. If not found on the GameServer, has no impact.
// Order: Sort by "Ascending" or "Descending". Default is "Descending" so bigger count is preferred.
// "Ascending" would be smaller count is preferred.
type Priority struct {
PriorityType string `json:"priorityType"`
Key string `json:"key"`
Order string `json:"order"`
}

// Validate returns if the Priority is valid.
func (p *Priority) validate(field string) ([]metav1.StatusCause, bool) {
var causes []metav1.StatusCause

if !(p.PriorityType == GameServerAllocationPriorityCounter || p.PriorityType == GameServerAllocationPriorityList) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: "Priority.Sort must be either `Counter` or `List`",
Field: field,
})
}

if !(p.Order == GameServerAllocationAscending || p.Order == GameServerAllocationDescending || p.Order == "") {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: "Priority.Order must be either `Ascending` or `Descending`",
Field: field,
})
}

return causes, len(causes) == 0
}

// GameServerAllocationStatus is the status for an GameServerAllocation resource
type GameServerAllocationStatus struct {
// GameServerState is the current state of an GameServerAllocation, e.g. Allocated, or UnAllocated
Expand Down Expand Up @@ -547,6 +597,22 @@ func (gsa *GameServerAllocation) Validate() ([]metav1.StatusCause, bool) {
}
}

if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) && (gsa.Spec.Priorities != nil) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: "Feature CountsAndLists must be enabled if Priorities is specified",
Field: "spec.priorities",
})
}

if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) && (gsa.Spec.Priorities != nil) {
for i := range gsa.Spec.Priorities {
if c, ok := gsa.Spec.Priorities[i].validate(fmt.Sprintf("spec.priorities[%d]", i)); !ok {
causes = append(causes, c...)
}
}
}

if c, ok := gsa.Spec.MetaPatch.Validate(); !ok {
causes = append(causes, c...)
}
Expand Down
165 changes: 164 additions & 1 deletion pkg/apis/allocation/v1/gameserverallocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ func TestGameServerAllocationApplyDefaults(t *testing.T) {

runtime.FeatureTestMutex.Lock()
defer runtime.FeatureTestMutex.Unlock()
assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s=true", runtime.FeaturePlayerAllocationFilter, runtime.FeatureStateAllocationFilter)))
assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s=true&%s=true", runtime.FeaturePlayerAllocationFilter, runtime.FeatureStateAllocationFilter, runtime.FeatureCountsAndLists)))

gsa = &GameServerAllocation{}
gsa.ApplyDefaults()

assert.Equal(t, agonesv1.GameServerStateReady, *gsa.Spec.Required.GameServerState)
assert.Equal(t, int64(0), gsa.Spec.Required.Players.MaxAvailable)
assert.Equal(t, int64(0), gsa.Spec.Required.Players.MinAvailable)
assert.Equal(t, []Priority(nil), gsa.Spec.Priorities)
assert.Nil(t, gsa.Spec.Priorities)
}

// nolint // Current lint duplicate threshold will consider this function is a duplication of the function TestGameServerAllocationSpecSelectors
Expand Down Expand Up @@ -351,6 +353,167 @@ func TestGameServerSelectorValidate(t *testing.T) {
}
}

func TestGameServerPriorityValidate(t *testing.T) {
t.Parallel()

runtime.FeatureTestMutex.Lock()
defer runtime.FeatureTestMutex.Unlock()
assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))

type expected struct {
valid bool
causeLen int
fields []string
}

fixtures := map[string]struct {
gsa *GameServerAllocation
expected expected
}{
"valid Counter Ascending": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "Counter",
Key: "Foo",
Order: "Ascending",
},
},
},
},
expected: expected{
valid: true,
causeLen: 0,
},
},
"valid Counter Descending": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "Counter",
Key: "Bar",
Order: "Descending",
},
},
},
},
expected: expected{
valid: true,
causeLen: 0,
},
},
"valid Counter empty Order": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "Counter",
Key: "Bar",
Order: "",
},
},
},
},
expected: expected{
valid: true,
causeLen: 0,
},
},
"invalid counter type and order": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "counter",
Key: "Babar",
Order: "descending",
},
},
},
},
expected: expected{
valid: false,
causeLen: 2,
},
},
"valid List Ascending": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "List",
Key: "Baz",
Order: "Ascending",
},
},
},
},
expected: expected{
valid: true,
causeLen: 0,
},
},
"valid List Descending": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "List",
Key: "Blerg",
Order: "Descending",
},
},
},
},
expected: expected{
valid: true,
causeLen: 0,
},
},
"valid List empty Order": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "List",
Key: "Blerg",
Order: "Ascending",
},
},
},
},
expected: expected{
valid: true,
causeLen: 0,
},
},
"invalid list type and order": {
gsa: &GameServerAllocation{
Spec: GameServerAllocationSpec{Priorities: []Priority{
{
PriorityType: "list",
Key: "Schmorg",
Order: "ascending",
},
},
},
},
expected: expected{
valid: false,
causeLen: 2,
},
},
}

for k, v := range fixtures {
t.Run(k, func(t *testing.T) {
v.gsa.ApplyDefaults()
causes, valid := v.gsa.Validate()
assert.Equal(t, v.expected.valid, valid)
assert.Len(t, causes, v.expected.causeLen)

for i := range v.expected.fields {
assert.Equal(t, v.expected.fields[i], causes[i].Field)
}
})
}
}

func TestMetaPatchValidate(t *testing.T) {
t.Parallel()

Expand Down
21 changes: 21 additions & 0 deletions pkg/apis/allocation/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5911f0c

Please sign in to comment.