From 31b7aae9e49537e212c089dff3c487590b18a4d6 Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Fri, 21 Apr 2023 21:09:45 +0000
Subject: [PATCH 1/7] Counts and Lists Actions on Game Server Allocation
---
pkg/apis/agones/v1/common.go | 6 +-
pkg/apis/agones/v1/gameserver.go | 80 ++++
pkg/apis/agones/v1/gameserver_test.go | 361 ++++++++++++++++++
.../allocation/v1/gameserverallocation.go | 47 +++
.../v1/gameserverallocation_test.go | 185 +++++++++
pkg/gameserverallocations/find.go | 14 +
6 files changed, 692 insertions(+), 1 deletion(-)
diff --git a/pkg/apis/agones/v1/common.go b/pkg/apis/agones/v1/common.go
index f1a5539dc4..bafe72e999 100644
--- a/pkg/apis/agones/v1/common.go
+++ b/pkg/apis/agones/v1/common.go
@@ -26,7 +26,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
)
-// Block of const Error messages
+// Block of const
const (
ErrContainerRequired = "Container is required when using multiple containers in the pod template"
ErrHostPort = "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"
@@ -34,6 +34,10 @@ const (
ErrContainerPortRequired = "ContainerPort must be defined for Dynamic and Static PortPolicies"
ErrContainerPortPassthrough = "ContainerPort cannot be specified with Passthrough PortPolicy"
ErrContainerNameInvalid = "Container must be empty or the name of a container in the pod template"
+ // GameServerAllocationIncrement is a Counter Action that indiciates the Counter's Count should be incremented at Allocation.
+ GameServerAllocationIncrement string = "Increment"
+ // GameServerAllocationIncrement is a Counter Action that indiciates the Counter's Count should be decremented at Allocation.
+ GameServerAllocationDecrement string = "Decrement"
)
// AggregatedPlayerStatus stores total player tracking values
diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go
index 392b250e3f..e5dbc8086e 100644
--- a/pkg/apis/agones/v1/gameserver.go
+++ b/pkg/apis/agones/v1/gameserver.go
@@ -926,3 +926,83 @@ func (gs *GameServer) Patch(delta *GameServer) ([]byte, error) {
result, err = json.Marshal(patch)
return result, errors.Wrapf(err, "error creating json for patch for GameServer %s", gs.ObjectMeta.Name)
}
+
+// UpdateCount increments or decrements a CounterStatus on a Game Server by the given amount.
+// TODO: Should we log on noncomplemention or return false? Issue #2716 asks for "silent no-op"
+func (gs *GameServer) UpdateCount(name string, action string, amount int64) {
+ if !(action == GameServerAllocationIncrement || action == GameServerAllocationDecrement) || amount < 0 {
+ return
+ }
+ if counter, ok := gs.Status.Counters[name]; ok {
+ cnt := counter.Count
+ if action == GameServerAllocationIncrement {
+ cnt += amount
+ }
+ if action == GameServerAllocationDecrement {
+ cnt -= amount
+ }
+ // TODO: Can we force the Capacity to update first before updating the Count if CounterAction updates both?
+ if (cnt < 0) || (cnt > counter.Capacity) {
+ return
+ }
+ counter.Count = cnt
+ gs.Status.Counters[name] = counter
+ }
+}
+
+// UpdateCounterCapacity updates the CounterStatus Capacity to the given capacity. Silent no-op.
+// TODO: Do anything if Capacity is updated to be less than the current Count?
+func (gs *GameServer) UpdateCounterCapacity(name string, capacity int64) {
+ if capacity < 0 {
+ return
+ }
+ if counter, ok := gs.Status.Counters[name]; ok {
+ counter.Capacity = capacity
+ gs.Status.Counters[name] = counter
+ }
+}
+
+// UpdateListCapacity updates the ListStatus Capacity to the given capacity. Silent no-op.
+func (gs *GameServer) UpdateListCapacity(name string, capacity int64) {
+ if capacity < 0 || capacity > 1000 {
+ return
+ }
+ if list, ok := gs.Status.Lists[name]; ok {
+ list.Capacity = capacity
+ gs.Status.Lists[name] = list
+ }
+}
+
+// AppendListValues adds given the values to the ListStatus Values list. Any duplicates are silently
+// dropped. Silent no-op.
+func (gs *GameServer) AppendListValues(name string, values []string) {
+ if len(values) == 0 {
+ return
+ }
+ if list, ok := gs.Status.Lists[name]; ok {
+ vals := []string{}
+ // Maintain ordering so new values are appended to the end of list.Values
+ vals = append(vals, list.Values...)
+ vals = append(vals, values...)
+ vals = removeDuplicates(vals)
+ if (len(vals) > int(list.Capacity)) || (len(vals) == len(list.Values)) {
+ return
+ }
+ // TODO: Do we need deepcopy here?
+ list.Values = vals
+ gs.Status.Lists[name] = list
+ }
+}
+
+// removeDuplicates removes any duplicate values from a list. Returns new list with unique values only.
+func removeDuplicates(values []string) []string {
+ listMap := make(map[string]bool)
+ uniqueValues := []string{}
+ for _, v := range values {
+ if _, ok := listMap[v]; !ok {
+ uniqueValues = append(uniqueValues, v)
+ listMap[v] = true
+ }
+ }
+ return uniqueValues
+}
diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go
index 0123df37dd..526e04216e 100644
--- a/pkg/apis/agones/v1/gameserver_test.go
+++ b/pkg/apis/agones/v1/gameserver_test.go
@@ -1635,3 +1635,364 @@ func defaultGameServer() *GameServer {
},
}, Status: GameServerStatus{State: GameServerStateCreating}}
}
+
+func TestGameServerUpdateCount(t *testing.T) {
+ t.Parallel()
+
+ testCases := map[string]struct {
+ gs GameServer
+ name string
+ action string
+ amount int64
+ want CounterStatus
+ }{
+ "counter not in game server silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "foos": {
+ Count: 0,
+ Capacity: 100,
+ }}}},
+ name: "foo",
+ action: "Increment",
+ amount: 1,
+ },
+ "amount less than zero silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "foos": {
+ Count: 1,
+ Capacity: 100,
+ }}}},
+ name: "foos",
+ action: "Decrement",
+ amount: -1,
+ want: CounterStatus{
+ Count: 1,
+ Capacity: 100,
+ },
+ },
+ "increment by 1": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "players": {
+ Count: 0,
+ Capacity: 100,
+ }}}},
+ name: "players",
+ action: "Increment",
+ amount: 1,
+ want: CounterStatus{
+ Count: 1,
+ Capacity: 100,
+ },
+ },
+ "decrement by 10": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "bars": {
+ Count: 99,
+ Capacity: 100,
+ }}}},
+ name: "bars",
+ action: "Decrement",
+ amount: 10,
+ want: CounterStatus{
+ Count: 89,
+ Capacity: 100,
+ },
+ },
+ "incorrect action silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "bazes": {
+ Count: 99,
+ Capacity: 100,
+ }}}},
+ name: "bazes",
+ action: "decrement",
+ amount: 10,
+ want: CounterStatus{
+ Count: 99,
+ Capacity: 100,
+ },
+ },
+ "decrement beyond count silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "baz": {
+ Count: 99,
+ Capacity: 100,
+ }}}},
+ name: "baz",
+ action: "Decrement",
+ amount: 100,
+ want: CounterStatus{
+ Count: 99,
+ Capacity: 100,
+ },
+ },
+ "increment beyond capacity silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "splayers": {
+ Count: 99,
+ Capacity: 100,
+ }}}},
+ name: "splayers",
+ action: "Increment",
+ amount: 2,
+ want: CounterStatus{
+ Count: 99,
+ Capacity: 100,
+ },
+ },
+ }
+
+ for test, testCase := range testCases {
+ t.Run(test, func(t *testing.T) {
+ testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount)
+ if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
+ assert.Equal(t, testCase.want, counter)
+ }
+ })
+ }
+}
+
+func TestGameServerUpdateCounterCapacity(t *testing.T) {
+ t.Parallel()
+
+ testCases := map[string]struct {
+ gs GameServer
+ name string
+ capacity int64
+ want CounterStatus
+ }{
+ "counter not in game server silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "foos": {
+ Count: 0,
+ Capacity: 100,
+ }}}},
+ name: "foo",
+ capacity: 1000,
+ },
+ "capacity less than zero silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "foos": {
+ Count: 0,
+ Capacity: 100,
+ }}}},
+ name: "foos",
+ capacity: -1000,
+ want: CounterStatus{
+ Count: 0,
+ Capacity: 100,
+ },
+ },
+ "update capacity": {
+ gs: GameServer{Status: GameServerStatus{
+ Counters: map[string]CounterStatus{
+ "sessions": {
+ Count: 0,
+ Capacity: 100,
+ }}}},
+ name: "sessions",
+ capacity: 9223372036854775807,
+ want: CounterStatus{
+ Count: 0,
+ Capacity: 9223372036854775807,
+ },
+ },
+ }
+
+ for test, testCase := range testCases {
+ t.Run(test, func(t *testing.T) {
+ testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity)
+ if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
+ assert.Equal(t, testCase.want, counter)
+ }
+ })
+ }
+}
+
+func TestGameServerUpdateListCapacity(t *testing.T) {
+ t.Parallel()
+
+ testCases := map[string]struct {
+ gs GameServer
+ name string
+ capacity int64
+ want ListStatus
+ }{
+ "list not in game server silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "things": {
+ Values: []string{},
+ Capacity: 100,
+ }}}},
+ name: "thing",
+ capacity: 1000,
+ },
+ "update list capacity": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "things": {
+ Values: []string{},
+ Capacity: 100,
+ }}}},
+ name: "things",
+ capacity: 1000,
+ want: ListStatus{
+ Values: []string{},
+ Capacity: 1000,
+ },
+ },
+ "list capacity above max silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "slings": {
+ Values: []string{},
+ Capacity: 100,
+ }}}},
+ name: "slings",
+ capacity: 10000,
+ want: ListStatus{
+ Values: []string{},
+ Capacity: 100,
+ },
+ },
+ "list capacity less than zero silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "flings": {
+ Values: []string{},
+ Capacity: 999,
+ }}}},
+ name: "flings",
+ capacity: -100,
+ want: ListStatus{
+ Values: []string{},
+ Capacity: 999,
+ },
+ },
+ }
+
+ for test, testCase := range testCases {
+ t.Run(test, func(t *testing.T) {
+ testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity)
+ if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
+ assert.Equal(t, testCase.want, list)
+ }
+ })
+ }
+}
+
+func TestGameServerAppendListValues(t *testing.T) {
+ t.Parallel()
+
+ testCases := map[string]struct {
+ gs GameServer
+ name string
+ values []string
+ want ListStatus
+ }{
+ "list not in game server silent no-op": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "things": {
+ Values: []string{},
+ Capacity: 100,
+ }}}},
+ name: "thing",
+ values: []string{"thing1", "thing2", "thing3"},
+ },
+ "append values": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "things": {
+ Values: []string{"thing1"},
+ Capacity: 100,
+ }}}},
+ name: "things",
+ values: []string{"thing2", "thing3"},
+ want: ListStatus{
+ Values: []string{"thing1", "thing2", "thing3"},
+ Capacity: 100,
+ },
+ },
+ "append values with duplicates": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "games": {
+ Values: []string{"game0"},
+ Capacity: 10,
+ }}}},
+ name: "games",
+ values: []string{"game1", "game2", "game2", "game1"},
+ want: ListStatus{
+ Values: []string{"game0", "game1", "game2"},
+ Capacity: 10,
+ },
+ },
+ "append values with duplicates in original list": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "objects": {
+ Values: []string{"object1", "object2"},
+ Capacity: 10,
+ }}}},
+ name: "objects",
+ values: []string{"object2", "object1", "object3", "object3"},
+ want: ListStatus{
+ Values: []string{"object1", "object2", "object3"},
+ Capacity: 10,
+ },
+ },
+ }
+
+ for test, testCase := range testCases {
+ t.Run(test, func(t *testing.T) {
+ testCase.gs.AppendListValues(testCase.name, testCase.values)
+ if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
+ assert.Equal(t, testCase.want, list)
+ }
+ })
+ }
+}
+
+func TestRemoveDuplicates(t *testing.T) {
+ t.Parallel()
+
+ testCases := map[string]struct {
+ values []string
+ want []string
+ }{
+ "empty string array": {
+ values: []string{},
+ want: []string{},
+ },
+ "no duplicates": {
+ values: []string{"one", "two", "three"},
+ want: []string{"one", "two", "three"},
+ },
+ "remove one duplicate": {
+ values: []string{"one", "one", "one"},
+ want: []string{"one"},
+ },
+ "remove multiple duplicates": {
+ values: []string{"one", "two", "one", "one", "two"},
+ want: []string{"one", "two"},
+ },
+ }
+
+ for test, testCase := range testCases {
+ t.Run(test, func(t *testing.T) {
+ got := removeDuplicates(testCase.values)
+ assert.Equal(t, testCase.want, got)
+ })
+ }
+}
diff --git a/pkg/apis/allocation/v1/gameserverallocation.go b/pkg/apis/allocation/v1/gameserverallocation.go
index 7a98c36d4b..4ae83540c8 100644
--- a/pkg/apis/allocation/v1/gameserverallocation.go
+++ b/pkg/apis/allocation/v1/gameserverallocation.go
@@ -109,6 +109,12 @@ type GameServerAllocationSpec struct {
// MetaPatch is optional custom metadata that is added to the game server at allocation
// You can use this to tell the server necessary session data
MetaPatch MetaPatch `json:"metadata,omitempty"`
+
+ // (Alpha, CountsAndLists feature flag) Counters and Lists provide a set of actions to perform
+ // on Counters and Lists during allocation.
+ // +optional
+ Counters map[string]CounterAction `json:"counters,omitempty"`
+ Lists map[string]ListAction `json:"lists,omitempty"`
}
// GameServerSelector contains all the filter options for selecting
@@ -166,6 +172,24 @@ type ListSelector struct {
MaxAvailable int64 `json:"maxAvailable"`
}
+// CounterAction is an optional action that can be performed on a Counter at allocation.
+// Action: "Increment" or "Decrement" the Counter's Count (optional). Must also define the Amount.
+// Amount: The amount to increment or decrement the Count (optional). Must be a positive integer.
+// Capacity: Update the maximum capacity of the Counter to this number (optional). Min 0, Max int64.
+type CounterAction struct {
+ Action *string `json:"action,omitempty"`
+ Amount *int64 `json:"amount,omitempty"`
+ Capacity *int64 `json:"capacity,omitempty"`
+}
+
+// ListAction is an optional action that can be performed on a List at allocation.
+// AddValues: Append values to a List's Values array (optional). Any duplicate values will be ignored.
+// Capacity: Update the maximum capacity of the Counter to this number (optional). Min 0, Max 1000.
+type ListAction struct {
+ AddValues *[]string `json:"addValues,omitempty"`
+ Capacity *int64 `json:"capacity,omitempty"`
+}
+
// ApplyDefaults applies default values
func (s *GameServerSelector) ApplyDefaults() {
if runtime.FeatureEnabled(runtime.FeatureStateAllocationFilter) {
@@ -270,6 +294,29 @@ func (s *GameServerSelector) matchCounters(gs *agonesv1.GameServer) bool {
return true
}
+// CounterActions attempts to peform any actions from the CounterAction on the GameServer Counter.
+// TODO: Silent no-op on all actions if one cannot be performed? Or silent no-op on the action that can't be performed?
+// Silent no-op if unable to perform the action.
+func (ca *CounterAction) CounterActions(counter string, gs *agonesv1.GameServer) {
+ if ca.Capacity != nil {
+ gs.UpdateCounterCapacity(counter, *ca.Capacity)
+ }
+ if ca.Action != nil && ca.Amount != nil {
+ gs.UpdateCount(counter, *ca.Action, *ca.Amount)
+ }
+}
+
+// ListActions attempts to peform any actions from the ListAction on the GameServer List.
+// Silent no-op if unable to perform the action.
+func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) {
+ if la.Capacity != nil {
+ gs.UpdateListCapacity(list, *la.Capacity)
+ }
+ if la.AddValues != nil {
+ gs.AppendListValues(list, *la.AddValues)
+ }
+}
+
// matchLists returns true if there is a match for the ListSelector in the GameServerStatus
func (s *GameServerSelector) matchLists(gs *agonesv1.GameServer) bool {
if gs.Status.Lists == nil {
diff --git a/pkg/apis/allocation/v1/gameserverallocation_test.go b/pkg/apis/allocation/v1/gameserverallocation_test.go
index b412efa7f1..7a9d9f1837 100644
--- a/pkg/apis/allocation/v1/gameserverallocation_test.go
+++ b/pkg/apis/allocation/v1/gameserverallocation_test.go
@@ -1016,6 +1016,191 @@ func TestGameServerSelectorMatches(t *testing.T) {
}
}
+// Helper function for creating int64 pointers
+func int64Pointer(x int64) *int64 {
+ return &x
+}
+
+func TestGameServerCounterActions(t *testing.T) {
+ t.Parallel()
+
+ runtime.FeatureTestMutex.Lock()
+ defer runtime.FeatureTestMutex.Unlock()
+ assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
+
+ DECREMENT := "Decrement"
+ INCREMENT := "Increment"
+
+ testScenarios := map[string]struct {
+ ca CounterAction
+ counter string
+ gs *agonesv1.GameServer
+ want *agonesv1.GameServer
+ }{
+ "update counter capacity": {
+ ca: CounterAction{
+ Capacity: int64Pointer(0),
+ },
+ counter: "mages",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "mages": {
+ Count: 1,
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "mages": {
+ Count: 1,
+ Capacity: 0,
+ }}}},
+ },
+ "update counter count": {
+ ca: CounterAction{
+ Action: &INCREMENT,
+ Amount: int64Pointer(10),
+ },
+ counter: "baddies",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "baddies": {
+ Count: 1,
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "baddies": {
+ Count: 11,
+ Capacity: 100,
+ }}}},
+ },
+ "update counter count and capacity": {
+ ca: CounterAction{
+ Action: &DECREMENT,
+ Amount: int64Pointer(10),
+ Capacity: int64Pointer(10),
+ },
+ counter: "heroes",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "heroes": {
+ Count: 11,
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "heroes": {
+ Count: 1,
+ Capacity: 10,
+ }}}},
+ },
+ }
+
+ for test, testScenario := range testScenarios {
+ t.Run(test, func(t *testing.T) {
+ testScenario.ca.CounterActions(testScenario.counter, testScenario.gs)
+ assert.Equal(t, testScenario.want, testScenario.gs)
+ })
+ }
+}
+
+func TestGameServerListActions(t *testing.T) {
+ t.Parallel()
+
+ runtime.FeatureTestMutex.Lock()
+ defer runtime.FeatureTestMutex.Unlock()
+ assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
+
+ testScenarios := map[string]struct {
+ la ListAction
+ list string
+ gs *agonesv1.GameServer
+ want *agonesv1.GameServer
+ }{
+ "update list capacity": {
+ la: ListAction{
+ Capacity: int64Pointer(0),
+ },
+ list: "pages",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "pages": {
+ Values: []string{"page1", "page2"},
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "pages": {
+ Values: []string{"page1", "page2"},
+ Capacity: 0,
+ }}}},
+ },
+ "update list values": {
+ la: ListAction{
+ AddValues: &[]string{"sage1", "sage3"},
+ },
+ list: "sages",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "sages": {
+ Values: []string{"sage1", "sage2"},
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "sages": {
+ Values: []string{"sage1", "sage2", "sage3"},
+ Capacity: 100,
+ }}}},
+ },
+ "update list values and capacity": {
+ la: ListAction{
+ AddValues: &[]string{"magician1", "magician3"},
+ Capacity: int64Pointer(42),
+ },
+ list: "magicians",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "magicians": {
+ Values: []string{"magician1", "magician2"},
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "magicians": {
+ Values: []string{"magician1", "magician2", "magician3"},
+ Capacity: 42,
+ }}}},
+ },
+ "update list values and capacity - value add fails": {
+ la: ListAction{
+ AddValues: &[]string{"fairy1", "fairy3"},
+ Capacity: int64Pointer(2),
+ },
+ list: "fairies",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "fairies": {
+ Values: []string{"fairy1", "fairy2"},
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Lists: map[string]agonesv1.ListStatus{
+ "fairies": {
+ Values: []string{"fairy1", "fairy2"},
+ Capacity: 2,
+ }}}},
+ },
+ }
+
+ for test, testScenario := range testScenarios {
+ t.Run(test, func(t *testing.T) {
+ testScenario.la.ListActions(testScenario.list, testScenario.gs)
+ assert.Equal(t, testScenario.want, testScenario.gs)
+ })
+ }
+}
+
func TestGameServerAllocationValidate(t *testing.T) {
t.Parallel()
diff --git a/pkg/gameserverallocations/find.go b/pkg/gameserverallocations/find.go
index d0c68aeb05..b278f26dcc 100644
--- a/pkg/gameserverallocations/find.go
+++ b/pkg/gameserverallocations/find.go
@@ -20,6 +20,7 @@ import (
"agones.dev/agones/pkg/apis"
agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
+ "agones.dev/agones/pkg/util/runtime"
"github.com/pkg/errors"
)
@@ -77,6 +78,19 @@ func findGameServerForAllocation(gsa *allocationv1.GameServerAllocation, list []
for j, sel := range gsa.Spec.Selectors {
if selectors[j] == nil && sel.Matches(gs) {
selectors[j] = &result{gs: gs, index: i}
+ // Once we have found a matching game server for allocation, perfom any Counter or List actions.
+ if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
+ if gsa.Spec.Counters != nil {
+ for c, ca := range gsa.Spec.Counters {
+ ca.CounterActions(c, gs)
+ }
+ }
+ if gsa.Spec.Lists != nil {
+ for l, la := range gsa.Spec.Lists {
+ la.ListActions(l, gs)
+ }
+ }
+ }
}
}
})
From 923f9ec03d64f8e261620f4826310326bd193484 Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Tue, 25 Apr 2023 21:15:15 +0000
Subject: [PATCH 2/7] Adds test for findGameServerForAllocation
---
pkg/gameserverallocations/find_test.go | 116 +++++++++++++++++++++++++
1 file changed, 116 insertions(+)
diff --git a/pkg/gameserverallocations/find_test.go b/pkg/gameserverallocations/find_test.go
index 2613bbca97..d95b7ef8c0 100644
--- a/pkg/gameserverallocations/find_test.go
+++ b/pkg/gameserverallocations/find_test.go
@@ -395,3 +395,119 @@ func TestFindGameServerForAllocationDistributed(t *testing.T) {
assert.FailNow(t, "We should get a different gameserver by now")
}
+
+func TestFindGameServerForAllocationCountListActions(t *testing.T) {
+ t.Parallel()
+
+ ONE := int64(1)
+ FORTY := int64(40)
+ INCREMENT := "Increment"
+ READY := agonesv1.GameServerStateReady
+
+ gs1 := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs1", Namespace: defaultNs, UID: "1"},
+ Status: agonesv1.GameServerStatus{NodeName: "node1", State: agonesv1.GameServerStateReady,
+ Lists: map[string]agonesv1.ListStatus{
+ "players": {
+ Values: []string{},
+ Capacity: 100,
+ },
+ },
+ Counters: map[string]agonesv1.CounterStatus{
+ "rooms": {
+ Count: 101,
+ Capacity: 1000,
+ }}}}
+
+ testScenarios := map[string]struct {
+ features string
+ gsa *allocationv1.GameServerAllocation
+ list []agonesv1.GameServer
+ listPointer []*agonesv1.GameServer
+ wantCounters map[string]agonesv1.CounterStatus
+ wantLists map[string]agonesv1.ListStatus
+ }{
+ "CounterActions increment and ListActions append and update capacity": {
+ features: fmt.Sprintf("%s=true&%s=true", runtime.FeatureCountsAndLists, runtime.FeatureStateAllocationFilter),
+ gsa: &allocationv1.GameServerAllocation{
+ ObjectMeta: metav1.ObjectMeta{Namespace: defaultNs},
+ Spec: allocationv1.GameServerAllocationSpec{
+ Selectors: []allocationv1.GameServerSelector{{
+ GameServerState: &READY,
+ }},
+ Scheduling: apis.Packed,
+ Counters: map[string]allocationv1.CounterAction{
+ "rooms": {
+ Action: &INCREMENT,
+ Amount: &ONE,
+ }},
+ Lists: map[string]allocationv1.ListAction{
+ "players": {
+ AddValues: &[]string{"x7un", "8inz"},
+ Capacity: &FORTY,
+ }}}},
+ list: []agonesv1.GameServer{gs1},
+ listPointer: []*agonesv1.GameServer{&gs1},
+ wantCounters: map[string]agonesv1.CounterStatus{
+ "rooms": {
+ Count: 102,
+ Capacity: 1000,
+ }},
+ wantLists: map[string]agonesv1.ListStatus{
+ "players": {
+ Values: []string{"x7un", "8inz"},
+ Capacity: 40,
+ }},
+ },
+ }
+
+ for test, testScenario := range testScenarios {
+ t.Run(test, func(t *testing.T) {
+ runtime.FeatureTestMutex.Lock()
+ defer runtime.FeatureTestMutex.Unlock()
+ // we always set the feature flag in all these tests, so always process it.
+ require.NoError(t, runtime.ParseFeatures(testScenario.features))
+
+ testScenario.gsa.ApplyDefaults()
+ _, ok := testScenario.gsa.Validate()
+ require.True(t, ok)
+
+ controller, m := newFakeController()
+ c := controller.allocator.allocationCache
+
+ m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, k8sruntime.Object, error) {
+ return true, &agonesv1.GameServerList{Items: testScenario.list}, nil
+ })
+
+ ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced)
+ defer cancel()
+
+ // This call initializes the cache
+ err := c.syncCache()
+ assert.Nil(t, err)
+
+ err = c.counter.Run(ctx, 0)
+ assert.Nil(t, err)
+
+ // This test assumes only one matching game server in the list (findGameServerForAllocation
+ // will always return the first match and does not remove it from the list).
+ foundGs, _, err := findGameServerForAllocation(testScenario.gsa, testScenario.listPointer)
+ assert.NoError(t, err)
+ changes := false
+ for counter, counterStatus := range testScenario.wantCounters {
+ if gsCounter, ok := foundGs.Status.Counters[counter]; ok {
+ changes = true
+ assert.Equal(t, counterStatus, gsCounter)
+ }
+ }
+ for list, listStatus := range testScenario.wantLists {
+ if gsList, ok := foundGs.Status.Lists[list]; ok {
+ changes = true
+ assert.Equal(t, listStatus, gsList)
+ }
+ }
+ if changes == false {
+ assert.Equal(t, testScenario.list[0], foundGs)
+ }
+ })
+ }
+}
From 24ca606e1716e09d7b9881d1d73709c28f808639 Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Thu, 27 Apr 2023 19:55:39 +0000
Subject: [PATCH 3/7] Updates ListAction field to avoid pointer to a list
---
pkg/apis/allocation/v1/gameserverallocation.go | 6 +++---
pkg/apis/allocation/v1/gameserverallocation_test.go | 6 +++---
pkg/gameserverallocations/find_test.go | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/pkg/apis/allocation/v1/gameserverallocation.go b/pkg/apis/allocation/v1/gameserverallocation.go
index 4ae83540c8..9d2351ac07 100644
--- a/pkg/apis/allocation/v1/gameserverallocation.go
+++ b/pkg/apis/allocation/v1/gameserverallocation.go
@@ -186,7 +186,7 @@ type CounterAction struct {
// AddValues: Append values to a List's Values array (optional). Any duplicate values will be ignored.
// Capacity: Update the maximum capacity of the Counter to this number (optional). Min 0, Max 1000.
type ListAction struct {
- AddValues *[]string `json:"addValues,omitempty"`
+ AddValues []string `json:"addValues,omitempty"`
Capacity *int64 `json:"capacity,omitempty"`
}
@@ -312,8 +312,8 @@ func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) {
if la.Capacity != nil {
gs.UpdateListCapacity(list, *la.Capacity)
}
- if la.AddValues != nil {
- gs.AppendListValues(list, *la.AddValues)
+ if la.AddValues != nil && len(la.AddValues) > 0 {
+ gs.AppendListValues(list, la.AddValues)
}
}
diff --git a/pkg/apis/allocation/v1/gameserverallocation_test.go b/pkg/apis/allocation/v1/gameserverallocation_test.go
index 7a9d9f1837..6f9c57da9b 100644
--- a/pkg/apis/allocation/v1/gameserverallocation_test.go
+++ b/pkg/apis/allocation/v1/gameserverallocation_test.go
@@ -1137,7 +1137,7 @@ func TestGameServerListActions(t *testing.T) {
},
"update list values": {
la: ListAction{
- AddValues: &[]string{"sage1", "sage3"},
+ AddValues: []string{"sage1", "sage3"},
},
list: "sages",
gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
@@ -1155,7 +1155,7 @@ func TestGameServerListActions(t *testing.T) {
},
"update list values and capacity": {
la: ListAction{
- AddValues: &[]string{"magician1", "magician3"},
+ AddValues: []string{"magician1", "magician3"},
Capacity: int64Pointer(42),
},
list: "magicians",
@@ -1174,7 +1174,7 @@ func TestGameServerListActions(t *testing.T) {
},
"update list values and capacity - value add fails": {
la: ListAction{
- AddValues: &[]string{"fairy1", "fairy3"},
+ AddValues: []string{"fairy1", "fairy3"},
Capacity: int64Pointer(2),
},
list: "fairies",
diff --git a/pkg/gameserverallocations/find_test.go b/pkg/gameserverallocations/find_test.go
index d95b7ef8c0..f9e4538a71 100644
--- a/pkg/gameserverallocations/find_test.go
+++ b/pkg/gameserverallocations/find_test.go
@@ -442,7 +442,7 @@ func TestFindGameServerForAllocationCountListActions(t *testing.T) {
}},
Lists: map[string]allocationv1.ListAction{
"players": {
- AddValues: &[]string{"x7un", "8inz"},
+ AddValues: []string{"x7un", "8inz"},
Capacity: &FORTY,
}}}},
list: []agonesv1.GameServer{gs1},
From 85ff20b1415269dc7508fa4e7f038e9641669e41 Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Thu, 27 Apr 2023 20:03:10 +0000
Subject: [PATCH 4/7] Generated Files
---
.../allocation/v1/gameserverallocation.go | 2 +-
.../allocation/v1/zz_generated.deepcopy.go | 71 +++++++++
.../Reference/agones_crd_api_reference.html | 147 ++++++++++++++++++
3 files changed, 219 insertions(+), 1 deletion(-)
diff --git a/pkg/apis/allocation/v1/gameserverallocation.go b/pkg/apis/allocation/v1/gameserverallocation.go
index 9d2351ac07..f2609406fc 100644
--- a/pkg/apis/allocation/v1/gameserverallocation.go
+++ b/pkg/apis/allocation/v1/gameserverallocation.go
@@ -187,7 +187,7 @@ type CounterAction struct {
// Capacity: Update the maximum capacity of the Counter to this number (optional). Min 0, Max 1000.
type ListAction struct {
AddValues []string `json:"addValues,omitempty"`
- Capacity *int64 `json:"capacity,omitempty"`
+ Capacity *int64 `json:"capacity,omitempty"`
}
// ApplyDefaults applies default values
diff --git a/pkg/apis/allocation/v1/zz_generated.deepcopy.go b/pkg/apis/allocation/v1/zz_generated.deepcopy.go
index e532bb88a6..f08064e317 100644
--- a/pkg/apis/allocation/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/allocation/v1/zz_generated.deepcopy.go
@@ -26,6 +26,37 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CounterAction) DeepCopyInto(out *CounterAction) {
+ *out = *in
+ if in.Action != nil {
+ in, out := &in.Action, &out.Action
+ *out = new(string)
+ **out = **in
+ }
+ if in.Amount != nil {
+ in, out := &in.Amount, &out.Amount
+ *out = new(int64)
+ **out = **in
+ }
+ if in.Capacity != nil {
+ in, out := &in.Capacity, &out.Capacity
+ *out = new(int64)
+ **out = **in
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CounterAction.
+func (in *CounterAction) DeepCopy() *CounterAction {
+ if in == nil {
+ return nil
+ }
+ out := new(CounterAction)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CounterSelector) DeepCopyInto(out *CounterSelector) {
*out = *in
@@ -128,6 +159,20 @@ func (in *GameServerAllocationSpec) DeepCopyInto(out *GameServerAllocationSpec)
}
}
in.MetaPatch.DeepCopyInto(&out.MetaPatch)
+ if in.Counters != nil {
+ in, out := &in.Counters, &out.Counters
+ *out = make(map[string]CounterAction, len(*in))
+ for key, val := range *in {
+ (*out)[key] = *val.DeepCopy()
+ }
+ }
+ if in.Lists != nil {
+ in, out := &in.Lists, &out.Lists
+ *out = make(map[string]ListAction, len(*in))
+ for key, val := range *in {
+ (*out)[key] = *val.DeepCopy()
+ }
+ }
return
}
@@ -203,6 +248,32 @@ func (in *GameServerSelector) DeepCopy() *GameServerSelector {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ListAction) DeepCopyInto(out *ListAction) {
+ *out = *in
+ if in.AddValues != nil {
+ in, out := &in.AddValues, &out.AddValues
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Capacity != nil {
+ in, out := &in.Capacity, &out.Capacity
+ *out = new(int64)
+ **out = **in
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListAction.
+func (in *ListAction) DeepCopy() *ListAction {
+ if in == nil {
+ return nil
+ }
+ out := new(ListAction)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ListSelector) DeepCopyInto(out *ListSelector) {
*out = *in
diff --git a/site/content/en/docs/Reference/agones_crd_api_reference.html b/site/content/en/docs/Reference/agones_crd_api_reference.html
index 45c685a2f2..df62bde126 100644
--- a/site/content/en/docs/Reference/agones_crd_api_reference.html
+++ b/site/content/en/docs/Reference/agones_crd_api_reference.html
@@ -5704,6 +5704,33 @@ GameServerAllocation
You can use this to tell the server necessary session data
+
+
+counters
+
+
+map[string]agones.dev/agones/pkg/apis/allocation/v1.CounterAction
+
+
+ |
+
+(Optional)
+ (Alpha, CountsAndLists feature flag) Counters and Lists provide a set of actions to perform
+on Counters and Lists during allocation.
+ |
+
+
+
+lists
+
+
+map[string]agones.dev/agones/pkg/apis/allocation/v1.ListAction
+
+
+ |
+
+ |
+
@@ -5721,6 +5748,58 @@ GameServerAllocation
+CounterAction
+
+
+(Appears on:
+GameServerAllocationSpec)
+
+
+
CounterAction is an optional action that can be performed on a Counter at allocation.
+Action: “Increment” or “Decrement” the Counter’s Count (optional). Must also define the Amount.
+Amount: The amount to increment or decrement the Count (optional). Must be a positive integer.
+Capacity: Update the maximum capacity of the Counter to this number (optional). Min 0, Max int64.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+action
+
+string
+
+ |
+
+ |
+
+
+
+amount
+
+int64
+
+ |
+
+ |
+
+
+
+capacity
+
+int64
+
+ |
+
+ |
+
+
+
CounterSelector
@@ -5903,6 +5982,33 @@
GameServerAllocationS
You can use this to tell the server necessary session data
+
+
+counters
+
+
+map[string]agones.dev/agones/pkg/apis/allocation/v1.CounterAction
+
+
+ |
+
+(Optional)
+ (Alpha, CountsAndLists feature flag) Counters and Lists provide a set of actions to perform
+on Counters and Lists during allocation.
+ |
+
+
+
+lists
+
+
+map[string]agones.dev/agones/pkg/apis/allocation/v1.ListAction
+
+
+ |
+
+ |
+
GameServerAllocationState
@@ -6103,6 +6209,47 @@ GameServerSelector
+ListAction
+
+
+(Appears on:
+GameServerAllocationSpec)
+
+
+
ListAction is an optional action that can be performed on a List at allocation.
+AddValues: Append values to a List’s Values array (optional). Any duplicate values will be ignored.
+Capacity: Update the maximum capacity of the Counter to this number (optional). Min 0, Max 1000.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+addValues
+
+[]string
+
+ |
+
+ |
+
+
+
+capacity
+
+int64
+
+ |
+
+ |
+
+
+
ListSelector
From 7707ce6705002a755ededdb53cd31728f7823c44 Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Tue, 2 May 2023 16:41:02 +0000
Subject: [PATCH 5/7] Moves counter and list action kick to the
Allocator.goAdds logging of any counter or list actions as warning events
---
pkg/apis/agones/v1/common.go | 2 +-
pkg/apis/agones/v1/gameserver.go | 48 ++++--
pkg/apis/agones/v1/gameserver_test.go | 148 +++++++++++++----
.../allocation/v1/gameserverallocation.go | 33 +++-
.../v1/gameserverallocation_test.go | 52 ++++--
pkg/gameserverallocations/allocator.go | 25 ++-
pkg/gameserverallocations/allocator_test.go | 156 +++++++++++++++++-
pkg/gameserverallocations/find.go | 14 --
pkg/gameserverallocations/find_test.go | 116 -------------
9 files changed, 389 insertions(+), 205 deletions(-)
diff --git a/pkg/apis/agones/v1/common.go b/pkg/apis/agones/v1/common.go
index bafe72e999..c38e546c57 100644
--- a/pkg/apis/agones/v1/common.go
+++ b/pkg/apis/agones/v1/common.go
@@ -26,7 +26,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
)
-// Block of const
+// nolint:stylecheck
const (
ErrContainerRequired = "Container is required when using multiple containers in the pod template"
ErrHostPort = "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"
diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go
index e5dbc8086e..4d791c224b 100644
--- a/pkg/apis/agones/v1/gameserver.go
+++ b/pkg/apis/agones/v1/gameserver.go
@@ -928,56 +928,67 @@ func (gs *GameServer) Patch(delta *GameServer) ([]byte, error) {
}
// UpdateCount increments or decrements a CounterStatus on a Game Server by the given amount.
-// TODO: Should we log on noncomplemention or return false? Issue #2716 asks for "silent no-op"
-func (gs *GameServer) UpdateCount(name string, action string, amount int64) {
+func (gs *GameServer) UpdateCount(name string, action string, amount int64) error {
+ err := fmt.Errorf("unable to UpdateCount: Name %s, Action %s, Amount %d", name, action, amount)
if !(action == GameServerAllocationIncrement || action == GameServerAllocationDecrement) || amount < 0 {
- return
+ return err
}
if counter, ok := gs.Status.Counters[name]; ok {
cnt := counter.Count
if action == GameServerAllocationIncrement {
cnt += amount
+ // only check for Count > Capacity when incrementing
+ if cnt > counter.Capacity {
+ return err
+ }
}
if action == GameServerAllocationDecrement {
cnt -= amount
}
- // TODO: Can we force the Capacity to update first before updating the Count if CounterAction updates both?
- if (cnt < 0) || (cnt > counter.Capacity) {
- return
+ if cnt < 0 {
+ return err
}
counter.Count = cnt
gs.Status.Counters[name] = counter
+ return nil
}
+ return err
}
-// UpdateCounterCapacity updates the CounterStatus Capacity to the given capacity. Silent no-op.
-// TODO: Do anything if Capacity is updated to be less than the current Count?
-func (gs *GameServer) UpdateCounterCapacity(name string, capacity int64) {
+// UpdateCounterCapacity updates the CounterStatus Capacity to the given capacity.
+func (gs *GameServer) UpdateCounterCapacity(name string, capacity int64) error {
+ err := fmt.Errorf("unable to UpdateCounterCapacity: Name %s, Capacity %d", name, capacity)
if capacity < 0 {
- return
+ return err
}
if counter, ok := gs.Status.Counters[name]; ok {
counter.Capacity = capacity
gs.Status.Counters[name] = counter
+ return nil
}
+ return err
}
-// UpdateListCapacity updates the ListStatus Capacity to the given capacity. Silent no-op.
-func (gs *GameServer) UpdateListCapacity(name string, capacity int64) {
+// UpdateListCapacity updates the ListStatus Capacity to the given capacity.
+func (gs *GameServer) UpdateListCapacity(name string, capacity int64) error {
+ err := fmt.Errorf("unable to UpdateListCapacity: Name %s, Capacity %d", name, capacity)
if capacity < 0 || capacity > 1000 {
- return
+ return err
}
if list, ok := gs.Status.Lists[name]; ok {
list.Capacity = capacity
gs.Status.Lists[name] = list
+ return nil
}
+ return err
}
// AppendListValues adds given the values to the ListStatus Values list. Any duplicates are silently
-// dropped. Silent no-op.
-func (gs *GameServer) AppendListValues(name string, values []string) {
+// dropped.
+func (gs *GameServer) AppendListValues(name string, values []string) error {
+ err := fmt.Errorf("unable to AppendListValues: Name %s, Values %s", name, values)
if len(values) == 0 {
- return
+ return err
}
if list, ok := gs.Status.Lists[name]; ok {
vals := []string{}
@@ -986,12 +997,13 @@ func (gs *GameServer) AppendListValues(name string, values []string) {
vals = append(vals, values...)
vals = removeDuplicates(vals)
if (len(vals) > int(list.Capacity)) || (len(vals) == len(list.Values)) {
- return
+ return err
}
- // TODO: Do we need deepcopy here?
list.Values = vals
gs.Status.Lists[name] = list
+ return nil
}
+ return err
}
// removeDuplicates removes any duplicate values from a list. Returns new list with unique values only.
diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go
index 526e04216e..0f3fedcb9d 100644
--- a/pkg/apis/agones/v1/gameserver_test.go
+++ b/pkg/apis/agones/v1/gameserver_test.go
@@ -1640,24 +1640,26 @@ func TestGameServerUpdateCount(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
- gs GameServer
- name string
- action string
- amount int64
- want CounterStatus
+ gs GameServer
+ name string
+ action string
+ amount int64
+ want CounterStatus
+ wantErr bool
}{
- "counter not in game server silent no-op": {
+ "counter not in game server no-op and error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"foos": {
Count: 0,
Capacity: 100,
}}}},
- name: "foo",
- action: "Increment",
- amount: 1,
+ name: "foo",
+ action: "Increment",
+ amount: 1,
+ wantErr: true,
},
- "amount less than zero silent no-op": {
+ "amount less than zero no-op and error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"foos": {
@@ -1671,6 +1673,7 @@ func TestGameServerUpdateCount(t *testing.T) {
Count: 1,
Capacity: 100,
},
+ wantErr: true,
},
"increment by 1": {
gs: GameServer{Status: GameServerStatus{
@@ -1686,6 +1689,7 @@ func TestGameServerUpdateCount(t *testing.T) {
Count: 1,
Capacity: 100,
},
+ wantErr: false,
},
"decrement by 10": {
gs: GameServer{Status: GameServerStatus{
@@ -1701,8 +1705,9 @@ func TestGameServerUpdateCount(t *testing.T) {
Count: 89,
Capacity: 100,
},
+ wantErr: false,
},
- "incorrect action silent no-op": {
+ "incorrect action no-op and error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"bazes": {
@@ -1716,8 +1721,9 @@ func TestGameServerUpdateCount(t *testing.T) {
Count: 99,
Capacity: 100,
},
+ wantErr: true,
},
- "decrement beyond count silent no-op": {
+ "decrement beyond count no-op and error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"baz": {
@@ -1731,8 +1737,9 @@ func TestGameServerUpdateCount(t *testing.T) {
Count: 99,
Capacity: 100,
},
+ wantErr: true,
},
- "increment beyond capacity silent no-op": {
+ "increment beyond capacity no-op and error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"splayers": {
@@ -1746,12 +1753,18 @@ func TestGameServerUpdateCount(t *testing.T) {
Count: 99,
Capacity: 100,
},
+ wantErr: true,
},
}
for test, testCase := range testCases {
t.Run(test, func(t *testing.T) {
- testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount)
+ err := testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount)
+ if err != nil {
+ assert.True(t, testCase.wantErr)
+ } else {
+ assert.False(t, testCase.wantErr)
+ }
if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
assert.Equal(t, testCase.want, counter)
}
@@ -1767,8 +1780,9 @@ func TestGameServerUpdateCounterCapacity(t *testing.T) {
name string
capacity int64
want CounterStatus
+ wantErr bool
}{
- "counter not in game server silent no-op": {
+ "counter not in game server no-op with error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"foos": {
@@ -1777,8 +1791,9 @@ func TestGameServerUpdateCounterCapacity(t *testing.T) {
}}}},
name: "foo",
capacity: 1000,
+ wantErr: true,
},
- "capacity less than zero silent no-op": {
+ "capacity less than zero no-op with error": {
gs: GameServer{Status: GameServerStatus{
Counters: map[string]CounterStatus{
"foos": {
@@ -1791,6 +1806,7 @@ func TestGameServerUpdateCounterCapacity(t *testing.T) {
Count: 0,
Capacity: 100,
},
+ wantErr: true,
},
"update capacity": {
gs: GameServer{Status: GameServerStatus{
@@ -1810,7 +1826,12 @@ func TestGameServerUpdateCounterCapacity(t *testing.T) {
for test, testCase := range testCases {
t.Run(test, func(t *testing.T) {
- testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity)
+ err := testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity)
+ if err != nil {
+ assert.True(t, testCase.wantErr)
+ } else {
+ assert.False(t, testCase.wantErr)
+ }
if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
assert.Equal(t, testCase.want, counter)
}
@@ -1826,8 +1847,9 @@ func TestGameServerUpdateListCapacity(t *testing.T) {
name string
capacity int64
want ListStatus
+ wantErr bool
}{
- "list not in game server silent no-op": {
+ "list not in game server no-op with error": {
gs: GameServer{Status: GameServerStatus{
Lists: map[string]ListStatus{
"things": {
@@ -1836,6 +1858,7 @@ func TestGameServerUpdateListCapacity(t *testing.T) {
}}}},
name: "thing",
capacity: 1000,
+ wantErr: true,
},
"update list capacity": {
gs: GameServer{Status: GameServerStatus{
@@ -1850,8 +1873,9 @@ func TestGameServerUpdateListCapacity(t *testing.T) {
Values: []string{},
Capacity: 1000,
},
+ wantErr: false,
},
- "list capacity above max silent no-op": {
+ "list capacity above max no-op with error": {
gs: GameServer{Status: GameServerStatus{
Lists: map[string]ListStatus{
"slings": {
@@ -1864,8 +1888,9 @@ func TestGameServerUpdateListCapacity(t *testing.T) {
Values: []string{},
Capacity: 100,
},
+ wantErr: true,
},
- "list capacity less than zero silent no-op": {
+ "list capacity less than zero no-op with error": {
gs: GameServer{Status: GameServerStatus{
Lists: map[string]ListStatus{
"flings": {
@@ -1878,12 +1903,18 @@ func TestGameServerUpdateListCapacity(t *testing.T) {
Values: []string{},
Capacity: 999,
},
+ wantErr: true,
},
}
for test, testCase := range testCases {
t.Run(test, func(t *testing.T) {
- testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity)
+ err := testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity)
+ if err != nil {
+ assert.True(t, testCase.wantErr)
+ } else {
+ assert.False(t, testCase.wantErr)
+ }
if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
assert.Equal(t, testCase.want, list)
}
@@ -1895,20 +1926,22 @@ func TestGameServerAppendListValues(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
- gs GameServer
- name string
- values []string
- want ListStatus
+ gs GameServer
+ name string
+ values []string
+ want ListStatus
+ wantErr bool
}{
- "list not in game server silent no-op": {
+ "list not in game server no-op with error": {
gs: GameServer{Status: GameServerStatus{
Lists: map[string]ListStatus{
"things": {
Values: []string{},
Capacity: 100,
}}}},
- name: "thing",
- values: []string{"thing1", "thing2", "thing3"},
+ name: "thing",
+ values: []string{"thing1", "thing2", "thing3"},
+ wantErr: true,
},
"append values": {
gs: GameServer{Status: GameServerStatus{
@@ -1923,8 +1956,9 @@ func TestGameServerAppendListValues(t *testing.T) {
Values: []string{"thing1", "thing2", "thing3"},
Capacity: 100,
},
+ wantErr: false,
},
- "append values with duplicates": {
+ "append values with silent drop of duplicates": {
gs: GameServer{Status: GameServerStatus{
Lists: map[string]ListStatus{
"games": {
@@ -1937,8 +1971,9 @@ func TestGameServerAppendListValues(t *testing.T) {
Values: []string{"game0", "game1", "game2"},
Capacity: 10,
},
+ wantErr: false,
},
- "append values with duplicates in original list": {
+ "append values with silent drop of duplicates in original list": {
gs: GameServer{Status: GameServerStatus{
Lists: map[string]ListStatus{
"objects": {
@@ -1951,12 +1986,63 @@ func TestGameServerAppendListValues(t *testing.T) {
Values: []string{"object1", "object2", "object3"},
Capacity: 10,
},
+ wantErr: false,
+ },
+ "append no values no-op with error": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "blings": {
+ Values: []string{"bling1"},
+ Capacity: 10,
+ }}}},
+ name: "blings",
+ values: []string{},
+ want: ListStatus{
+ Values: []string{"bling1"},
+ Capacity: 10,
+ },
+ wantErr: true,
+ },
+ "append only duplicates no-op with error": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "slings": {
+ Values: []string{"slings1", "sling2"},
+ Capacity: 4,
+ }}}},
+ name: "blings",
+ values: []string{},
+ want: ListStatus{
+ Values: []string{"slings1", "sling2"},
+ Capacity: 4,
+ },
+ wantErr: true,
+ },
+ "append too many values no-op with error": {
+ gs: GameServer{Status: GameServerStatus{
+ Lists: map[string]ListStatus{
+ "bananaslugs": {
+ Values: []string{"bananaslugs1", "bananaslug2", "bananaslug3"},
+ Capacity: 5,
+ }}}},
+ name: "bananaslugs",
+ values: []string{"bananaslug4", "bananaslug5", "bananaslug6"},
+ want: ListStatus{
+ Values: []string{"bananaslugs1", "bananaslug2", "bananaslug3"},
+ Capacity: 5,
+ },
+ wantErr: true,
},
}
for test, testCase := range testCases {
t.Run(test, func(t *testing.T) {
- testCase.gs.AppendListValues(testCase.name, testCase.values)
+ err := testCase.gs.AppendListValues(testCase.name, testCase.values)
+ if err != nil {
+ assert.True(t, testCase.wantErr)
+ } else {
+ assert.False(t, testCase.wantErr)
+ }
if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
assert.Equal(t, testCase.want, list)
}
diff --git a/pkg/apis/allocation/v1/gameserverallocation.go b/pkg/apis/allocation/v1/gameserverallocation.go
index f2609406fc..279ad2b789 100644
--- a/pkg/apis/allocation/v1/gameserverallocation.go
+++ b/pkg/apis/allocation/v1/gameserverallocation.go
@@ -295,26 +295,41 @@ func (s *GameServerSelector) matchCounters(gs *agonesv1.GameServer) bool {
}
// CounterActions attempts to peform any actions from the CounterAction on the GameServer Counter.
-// TODO: Silent no-op on all actions if one cannot be performed? Or silent no-op on the action that can't be performed?
-// Silent no-op if unable to perform the action.
-func (ca *CounterAction) CounterActions(counter string, gs *agonesv1.GameServer) {
+// Returns a string list of any actions that could not be performed.
+func (ca *CounterAction) CounterActions(counter string, gs *agonesv1.GameServer) []string {
+ errors := []string{}
if ca.Capacity != nil {
- gs.UpdateCounterCapacity(counter, *ca.Capacity)
+ capErr := gs.UpdateCounterCapacity(counter, *ca.Capacity)
+ if capErr != nil {
+ errors = append(errors, capErr.Error())
+ }
}
if ca.Action != nil && ca.Amount != nil {
- gs.UpdateCount(counter, *ca.Action, *ca.Amount)
+ cntErr := gs.UpdateCount(counter, *ca.Action, *ca.Amount)
+ if cntErr != nil {
+ errors = append(errors, cntErr.Error())
+ }
}
+ return errors
}
// ListActions attempts to peform any actions from the ListAction on the GameServer List.
-// Silent no-op if unable to perform the action.
-func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) {
+// Returns a string list of any actions that could not be performed.
+func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) []string {
+ errors := []string{}
if la.Capacity != nil {
- gs.UpdateListCapacity(list, *la.Capacity)
+ capErr := gs.UpdateListCapacity(list, *la.Capacity)
+ if capErr != nil {
+ errors = append(errors, capErr.Error())
+ }
}
if la.AddValues != nil && len(la.AddValues) > 0 {
- gs.AppendListValues(list, la.AddValues)
+ cntErr := gs.AppendListValues(list, la.AddValues)
+ if cntErr != nil {
+ errors = append(errors, cntErr.Error())
+ }
}
+ return errors
}
// matchLists returns true if there is a match for the ListSelector in the GameServerStatus
diff --git a/pkg/apis/allocation/v1/gameserverallocation_test.go b/pkg/apis/allocation/v1/gameserverallocation_test.go
index 6f9c57da9b..8a0d81d93c 100644
--- a/pkg/apis/allocation/v1/gameserverallocation_test.go
+++ b/pkg/apis/allocation/v1/gameserverallocation_test.go
@@ -1032,10 +1032,11 @@ func TestGameServerCounterActions(t *testing.T) {
INCREMENT := "Increment"
testScenarios := map[string]struct {
- ca CounterAction
- counter string
- gs *agonesv1.GameServer
- want *agonesv1.GameServer
+ ca CounterAction
+ counter string
+ gs *agonesv1.GameServer
+ want *agonesv1.GameServer
+ errLength int
}{
"update counter capacity": {
ca: CounterAction{
@@ -1054,6 +1055,28 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 1,
Capacity: 0,
}}}},
+ errLength: 0,
+ },
+ "fail update counter capacity and count": {
+ ca: CounterAction{
+ Action: &INCREMENT,
+ Amount: int64Pointer(10),
+ Capacity: int64Pointer(-1),
+ },
+ counter: "sages",
+ gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "sages": {
+ Count: 99,
+ Capacity: 100,
+ }}}},
+ want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
+ Counters: map[string]agonesv1.CounterStatus{
+ "sages": {
+ Count: 99,
+ Capacity: 100,
+ }}}},
+ errLength: 2,
},
"update counter count": {
ca: CounterAction{
@@ -1073,6 +1096,7 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 11,
Capacity: 100,
}}}},
+ errLength: 0,
},
"update counter count and capacity": {
ca: CounterAction{
@@ -1093,13 +1117,15 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 1,
Capacity: 10,
}}}},
+ errLength: 0,
},
}
for test, testScenario := range testScenarios {
t.Run(test, func(t *testing.T) {
- testScenario.ca.CounterActions(testScenario.counter, testScenario.gs)
+ errs := testScenario.ca.CounterActions(testScenario.counter, testScenario.gs)
assert.Equal(t, testScenario.want, testScenario.gs)
+ assert.Equal(t, testScenario.errLength, len(errs))
})
}
}
@@ -1112,10 +1138,11 @@ func TestGameServerListActions(t *testing.T) {
assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
testScenarios := map[string]struct {
- la ListAction
- list string
- gs *agonesv1.GameServer
- want *agonesv1.GameServer
+ la ListAction
+ list string
+ gs *agonesv1.GameServer
+ want *agonesv1.GameServer
+ errLength int
}{
"update list capacity": {
la: ListAction{
@@ -1134,6 +1161,7 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"page1", "page2"},
Capacity: 0,
}}}},
+ errLength: 0,
},
"update list values": {
la: ListAction{
@@ -1152,6 +1180,7 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"sage1", "sage2", "sage3"},
Capacity: 100,
}}}},
+ errLength: 0,
},
"update list values and capacity": {
la: ListAction{
@@ -1171,6 +1200,7 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"magician1", "magician2", "magician3"},
Capacity: 42,
}}}},
+ errLength: 0,
},
"update list values and capacity - value add fails": {
la: ListAction{
@@ -1190,13 +1220,15 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"fairy1", "fairy2"},
Capacity: 2,
}}}},
+ errLength: 1,
},
}
for test, testScenario := range testScenarios {
t.Run(test, func(t *testing.T) {
- testScenario.la.ListActions(testScenario.list, testScenario.gs)
+ errs := testScenario.la.ListActions(testScenario.list, testScenario.gs)
assert.Equal(t, testScenario.want, testScenario.gs)
+ assert.Equal(t, testScenario.errLength, len(errs))
})
}
}
diff --git a/pkg/gameserverallocations/allocator.go b/pkg/gameserverallocations/allocator.go
index 1b7ad87521..69895f384d 100644
--- a/pkg/gameserverallocations/allocator.go
+++ b/pkg/gameserverallocations/allocator.go
@@ -553,7 +553,7 @@ func (c *Allocator) allocationUpdateWorkers(ctx context.Context, workerCount int
for {
select {
case res := <-updateQueue:
- gs, err := c.applyAllocationToGameServer(ctx, res.request.gsa.Spec.MetaPatch, res.gs)
+ gs, err := c.applyAllocationToGameServer(ctx, res.request.gsa.Spec.MetaPatch, res.gs, res.request.gsa)
if err != nil {
if !k8serrors.IsConflict(errors.Cause(err)) {
// since we could not allocate, we should put it back
@@ -580,7 +580,7 @@ func (c *Allocator) allocationUpdateWorkers(ctx context.Context, workerCount int
// applyAllocationToGameServer patches the inputted GameServer with the allocation metadata changes, and updates it to the Allocated State.
// Returns the updated GameServer.
-func (c *Allocator) applyAllocationToGameServer(ctx context.Context, mp allocationv1.MetaPatch, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
+func (c *Allocator) applyAllocationToGameServer(ctx context.Context, mp allocationv1.MetaPatch, gs *agonesv1.GameServer, gsa *allocationv1.GameServerAllocation) (*agonesv1.GameServer, error) {
// patch ObjectMeta labels
if mp.Labels != nil {
if gs.ObjectMeta.Labels == nil {
@@ -607,6 +607,27 @@ func (c *Allocator) applyAllocationToGameServer(ctx context.Context, mp allocati
gs.ObjectMeta.Annotations[LastAllocatedAnnotationKey] = string(ts)
gs.Status.State = agonesv1.GameServerStateAllocated
+ // perfom any Counter or List actions
+ if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
+ actionErrors := []string{}
+ if gsa.Spec.Counters != nil {
+ for counter, ca := range gsa.Spec.Counters {
+ errs := ca.CounterActions(counter, gs)
+ actionErrors = append(actionErrors, errs...)
+ }
+ }
+ if gsa.Spec.Lists != nil {
+ for list, la := range gsa.Spec.Lists {
+ errs := la.ListActions(list, gs)
+ actionErrors = append(actionErrors, errs...)
+ }
+ }
+ // record any Counter or List action errors as a warning
+ if len(actionErrors) != 0 {
+ c.recorder.Event(gs, corev1.EventTypeWarning, string(runtime.FeatureCountsAndLists), fmt.Sprint(actionErrors))
+ }
+ }
+
return c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gs, metav1.UpdateOptions{})
}
diff --git a/pkg/gameserverallocations/allocator_test.go b/pkg/gameserverallocations/allocator_test.go
index 0f4a9ae3f8..48e17b7cf5 100644
--- a/pkg/gameserverallocations/allocator_test.go
+++ b/pkg/gameserverallocations/allocator_test.go
@@ -17,6 +17,7 @@ package gameserverallocations
import (
"context"
"errors"
+ "fmt"
"testing"
"time"
@@ -26,6 +27,7 @@ import (
multiclusterv1 "agones.dev/agones/pkg/apis/multicluster/v1"
"agones.dev/agones/pkg/gameservers"
agtesting "agones.dev/agones/pkg/testing"
+ "agones.dev/agones/pkg/util/runtime"
"github.com/heptiolabs/healthcheck"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -245,14 +247,14 @@ func TestAllocatorApplyAllocationToGameServer(t *testing.T) {
time.Second, 5*time.Second, 500*time.Millisecond,
)
- gs, err := allocator.applyAllocationToGameServer(ctx, allocationv1.MetaPatch{}, &agonesv1.GameServer{})
+ gs, err := allocator.applyAllocationToGameServer(ctx, allocationv1.MetaPatch{}, &agonesv1.GameServer{}, &allocationv1.GameServerAllocation{})
assert.NoError(t, err)
assert.Equal(t, agonesv1.GameServerStateAllocated, gs.Status.State)
assert.NotNil(t, gs.ObjectMeta.Annotations["agones.dev/last-allocated"])
var ts time.Time
assert.NoError(t, ts.UnmarshalText([]byte(gs.ObjectMeta.Annotations[LastAllocatedAnnotationKey])))
- gs, err = allocator.applyAllocationToGameServer(ctx, allocationv1.MetaPatch{Labels: map[string]string{"foo": "bar"}}, &agonesv1.GameServer{})
+ gs, err = allocator.applyAllocationToGameServer(ctx, allocationv1.MetaPatch{Labels: map[string]string{"foo": "bar"}}, &agonesv1.GameServer{}, &allocationv1.GameServerAllocation{})
assert.NoError(t, err)
assert.Equal(t, agonesv1.GameServerStateAllocated, gs.Status.State)
assert.Equal(t, "bar", gs.ObjectMeta.Labels["foo"])
@@ -260,7 +262,7 @@ func TestAllocatorApplyAllocationToGameServer(t *testing.T) {
gs, err = allocator.applyAllocationToGameServer(ctx,
allocationv1.MetaPatch{Labels: map[string]string{"foo": "bar"}, Annotations: map[string]string{"bar": "foo"}},
- &agonesv1.GameServer{})
+ &agonesv1.GameServer{}, &allocationv1.GameServerAllocation{})
assert.NoError(t, err)
assert.Equal(t, agonesv1.GameServerStateAllocated, gs.Status.State)
assert.Equal(t, "bar", gs.ObjectMeta.Labels["foo"])
@@ -268,6 +270,152 @@ func TestAllocatorApplyAllocationToGameServer(t *testing.T) {
assert.NotNil(t, gs.ObjectMeta.Annotations[LastAllocatedAnnotationKey])
}
+func TestAllocatorApplyAllocationToGameServerCountsListsActions(t *testing.T) {
+ t.Parallel()
+ m := agtesting.NewMocks()
+ ctx := context.Background()
+ mp := allocationv1.MetaPatch{}
+
+ m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, k8sruntime.Object, error) {
+ ua := action.(k8stesting.UpdateAction)
+ gs := ua.GetObject().(*agonesv1.GameServer)
+ return true, gs, nil
+ })
+
+ allocator := NewAllocator(m.AgonesInformerFactory.Multicluster().V1().GameServerAllocationPolicies(),
+ m.KubeInformerFactory.Core().V1().Secrets(),
+ m.AgonesClient.AgonesV1(), m.KubeClient,
+ NewAllocationCache(m.AgonesInformerFactory.Agones().V1().GameServers(), gameservers.NewPerNodeCounter(m.KubeInformerFactory, m.AgonesInformerFactory), healthcheck.NewHandler()),
+ time.Second, 5*time.Second, 500*time.Millisecond,
+ )
+
+ ONE := int64(1)
+ FORTY := int64(40)
+ THOUSAND := int64(1000)
+ INCREMENT := "Increment"
+ READY := agonesv1.GameServerStateReady
+
+ gs1 := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs1", Namespace: defaultNs, UID: "1"},
+ Status: agonesv1.GameServerStatus{NodeName: "node1", State: agonesv1.GameServerStateReady,
+ Lists: map[string]agonesv1.ListStatus{
+ "players": {
+ Values: []string{},
+ Capacity: 100,
+ },
+ },
+ Counters: map[string]agonesv1.CounterStatus{
+ "rooms": {
+ Count: 101,
+ Capacity: 1000,
+ }}}}
+ gs2 := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs2", Namespace: defaultNs, UID: "2"},
+ Status: agonesv1.GameServerStatus{NodeName: "node1", State: agonesv1.GameServerStateReady,
+ Lists: map[string]agonesv1.ListStatus{
+ "players": {
+ Values: []string{},
+ Capacity: 100,
+ },
+ },
+ Counters: map[string]agonesv1.CounterStatus{
+ "rooms": {
+ Count: 101,
+ Capacity: 1000,
+ }}}}
+
+ testScenarios := map[string]struct {
+ features string
+ gs *agonesv1.GameServer
+ gsa *allocationv1.GameServerAllocation
+ wantCounters map[string]agonesv1.CounterStatus
+ wantLists map[string]agonesv1.ListStatus
+ }{
+ "CounterActions increment and ListActions append and update capacity": {
+ features: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists),
+ gs: &gs1,
+ gsa: &allocationv1.GameServerAllocation{
+ ObjectMeta: metav1.ObjectMeta{Namespace: defaultNs},
+ Spec: allocationv1.GameServerAllocationSpec{
+ Selectors: []allocationv1.GameServerSelector{{
+ GameServerState: &READY,
+ }},
+ Scheduling: apis.Packed,
+ Counters: map[string]allocationv1.CounterAction{
+ "rooms": {
+ Action: &INCREMENT,
+ Amount: &ONE,
+ }},
+ Lists: map[string]allocationv1.ListAction{
+ "players": {
+ AddValues: []string{"x7un", "8inz"},
+ Capacity: &FORTY,
+ }}}},
+ wantCounters: map[string]agonesv1.CounterStatus{
+ "rooms": {
+ Count: 102,
+ Capacity: 1000,
+ }},
+ wantLists: map[string]agonesv1.ListStatus{
+ "players": {
+ Values: []string{"x7un", "8inz"},
+ Capacity: 40,
+ }},
+ },
+ "CounterActions and ListActions only update list capacity": {
+ features: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists),
+ gs: &gs2,
+ gsa: &allocationv1.GameServerAllocation{
+ ObjectMeta: metav1.ObjectMeta{Namespace: defaultNs},
+ Spec: allocationv1.GameServerAllocationSpec{
+ Selectors: []allocationv1.GameServerSelector{{
+ GameServerState: &READY,
+ }},
+ Scheduling: apis.Packed,
+ Counters: map[string]allocationv1.CounterAction{
+ "rooms": {
+ Action: &INCREMENT,
+ Amount: &THOUSAND,
+ }},
+ Lists: map[string]allocationv1.ListAction{
+ "players": {
+ AddValues: []string{"x7un", "8inz"},
+ Capacity: &ONE,
+ }}}},
+ wantCounters: map[string]agonesv1.CounterStatus{
+ "rooms": {
+ Count: 101,
+ Capacity: 1000,
+ }},
+ wantLists: map[string]agonesv1.ListStatus{
+ "players": {
+ Values: []string{},
+ Capacity: 1,
+ }},
+ },
+ }
+
+ for test, testScenario := range testScenarios {
+ t.Run(test, func(t *testing.T) {
+ runtime.FeatureTestMutex.Lock()
+ defer runtime.FeatureTestMutex.Unlock()
+ // we always set the feature flag in all these tests, so always process it.
+ require.NoError(t, runtime.ParseFeatures(testScenario.features))
+
+ foundGs, err := allocator.applyAllocationToGameServer(ctx, mp, testScenario.gs, testScenario.gsa)
+ assert.NoError(t, err)
+ for counter, counterStatus := range testScenario.wantCounters {
+ if gsCounter, ok := foundGs.Status.Counters[counter]; ok {
+ assert.Equal(t, counterStatus, gsCounter)
+ }
+ }
+ for list, listStatus := range testScenario.wantLists {
+ if gsList, ok := foundGs.Status.Lists[list]; ok {
+ assert.Equal(t, listStatus, gsList)
+ }
+ }
+ })
+ }
+}
+
func TestAllocationApplyAllocationError(t *testing.T) {
t.Parallel()
m := agtesting.NewMocks()
@@ -284,7 +432,7 @@ func TestAllocationApplyAllocationError(t *testing.T) {
time.Second, 5*time.Second, 500*time.Millisecond,
)
- gsa, err := allocator.applyAllocationToGameServer(ctx, allocationv1.MetaPatch{}, &agonesv1.GameServer{})
+ gsa, err := allocator.applyAllocationToGameServer(ctx, allocationv1.MetaPatch{}, &agonesv1.GameServer{}, &allocationv1.GameServerAllocation{})
logrus.WithError(err).WithField("gsa", gsa).WithField("test", t.Name()).Info("Allocation should fail")
assert.Error(t, err)
}
diff --git a/pkg/gameserverallocations/find.go b/pkg/gameserverallocations/find.go
index b278f26dcc..d0c68aeb05 100644
--- a/pkg/gameserverallocations/find.go
+++ b/pkg/gameserverallocations/find.go
@@ -20,7 +20,6 @@ import (
"agones.dev/agones/pkg/apis"
agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
- "agones.dev/agones/pkg/util/runtime"
"github.com/pkg/errors"
)
@@ -78,19 +77,6 @@ func findGameServerForAllocation(gsa *allocationv1.GameServerAllocation, list []
for j, sel := range gsa.Spec.Selectors {
if selectors[j] == nil && sel.Matches(gs) {
selectors[j] = &result{gs: gs, index: i}
- // Once we have found a matching game server for allocation, perfom any Counter or List actions.
- if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
- if gsa.Spec.Counters != nil {
- for c, ca := range gsa.Spec.Counters {
- ca.CounterActions(c, gs)
- }
- }
- if gsa.Spec.Lists != nil {
- for l, la := range gsa.Spec.Lists {
- la.ListActions(l, gs)
- }
- }
- }
}
}
})
diff --git a/pkg/gameserverallocations/find_test.go b/pkg/gameserverallocations/find_test.go
index f9e4538a71..2613bbca97 100644
--- a/pkg/gameserverallocations/find_test.go
+++ b/pkg/gameserverallocations/find_test.go
@@ -395,119 +395,3 @@ func TestFindGameServerForAllocationDistributed(t *testing.T) {
assert.FailNow(t, "We should get a different gameserver by now")
}
-
-func TestFindGameServerForAllocationCountListActions(t *testing.T) {
- t.Parallel()
-
- ONE := int64(1)
- FORTY := int64(40)
- INCREMENT := "Increment"
- READY := agonesv1.GameServerStateReady
-
- gs1 := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs1", Namespace: defaultNs, UID: "1"},
- Status: agonesv1.GameServerStatus{NodeName: "node1", State: agonesv1.GameServerStateReady,
- Lists: map[string]agonesv1.ListStatus{
- "players": {
- Values: []string{},
- Capacity: 100,
- },
- },
- Counters: map[string]agonesv1.CounterStatus{
- "rooms": {
- Count: 101,
- Capacity: 1000,
- }}}}
-
- testScenarios := map[string]struct {
- features string
- gsa *allocationv1.GameServerAllocation
- list []agonesv1.GameServer
- listPointer []*agonesv1.GameServer
- wantCounters map[string]agonesv1.CounterStatus
- wantLists map[string]agonesv1.ListStatus
- }{
- "CounterActions increment and ListActions append and update capacity": {
- features: fmt.Sprintf("%s=true&%s=true", runtime.FeatureCountsAndLists, runtime.FeatureStateAllocationFilter),
- gsa: &allocationv1.GameServerAllocation{
- ObjectMeta: metav1.ObjectMeta{Namespace: defaultNs},
- Spec: allocationv1.GameServerAllocationSpec{
- Selectors: []allocationv1.GameServerSelector{{
- GameServerState: &READY,
- }},
- Scheduling: apis.Packed,
- Counters: map[string]allocationv1.CounterAction{
- "rooms": {
- Action: &INCREMENT,
- Amount: &ONE,
- }},
- Lists: map[string]allocationv1.ListAction{
- "players": {
- AddValues: []string{"x7un", "8inz"},
- Capacity: &FORTY,
- }}}},
- list: []agonesv1.GameServer{gs1},
- listPointer: []*agonesv1.GameServer{&gs1},
- wantCounters: map[string]agonesv1.CounterStatus{
- "rooms": {
- Count: 102,
- Capacity: 1000,
- }},
- wantLists: map[string]agonesv1.ListStatus{
- "players": {
- Values: []string{"x7un", "8inz"},
- Capacity: 40,
- }},
- },
- }
-
- for test, testScenario := range testScenarios {
- t.Run(test, func(t *testing.T) {
- runtime.FeatureTestMutex.Lock()
- defer runtime.FeatureTestMutex.Unlock()
- // we always set the feature flag in all these tests, so always process it.
- require.NoError(t, runtime.ParseFeatures(testScenario.features))
-
- testScenario.gsa.ApplyDefaults()
- _, ok := testScenario.gsa.Validate()
- require.True(t, ok)
-
- controller, m := newFakeController()
- c := controller.allocator.allocationCache
-
- m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, k8sruntime.Object, error) {
- return true, &agonesv1.GameServerList{Items: testScenario.list}, nil
- })
-
- ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced)
- defer cancel()
-
- // This call initializes the cache
- err := c.syncCache()
- assert.Nil(t, err)
-
- err = c.counter.Run(ctx, 0)
- assert.Nil(t, err)
-
- // This test assumes only one matching game server in the list (findGameServerForAllocation
- // will always return the first match and does not remove it from the list).
- foundGs, _, err := findGameServerForAllocation(testScenario.gsa, testScenario.listPointer)
- assert.NoError(t, err)
- changes := false
- for counter, counterStatus := range testScenario.wantCounters {
- if gsCounter, ok := foundGs.Status.Counters[counter]; ok {
- changes = true
- assert.Equal(t, counterStatus, gsCounter)
- }
- }
- for list, listStatus := range testScenario.wantLists {
- if gsList, ok := foundGs.Status.Lists[list]; ok {
- changes = true
- assert.Equal(t, listStatus, gsList)
- }
- }
- if changes == false {
- assert.Equal(t, testScenario.list[0], foundGs)
- }
- })
- }
-}
From 17398eb9640fccfc46cf4264ae2a31c047d416af Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Fri, 5 May 2023 18:25:21 +0000
Subject: [PATCH 6/7] Updates actions to return single error
---
pkg/apis/agones/v1/common.go | 4 +-
pkg/apis/agones/v1/gameserver.go | 83 ++++++++++---------
pkg/apis/agones/v1/gameserver_test.go | 31 ++++---
.../allocation/v1/gameserverallocation.go | 23 ++---
.../v1/gameserverallocation_test.go | 48 ++++++-----
pkg/gameserverallocations/allocator.go | 17 ++--
6 files changed, 115 insertions(+), 91 deletions(-)
diff --git a/pkg/apis/agones/v1/common.go b/pkg/apis/agones/v1/common.go
index c38e546c57..12fcd3d212 100644
--- a/pkg/apis/agones/v1/common.go
+++ b/pkg/apis/agones/v1/common.go
@@ -26,7 +26,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
)
-// nolint:stylecheck
+// Block of const Error messages and GameServerAllocation Counter actions
const (
ErrContainerRequired = "Container is required when using multiple containers in the pod template"
ErrHostPort = "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"
@@ -36,7 +36,7 @@ const (
ErrContainerNameInvalid = "Container must be empty or the name of a container in the pod template"
// GameServerAllocationIncrement is a Counter Action that indiciates the Counter's Count should be incremented at Allocation.
GameServerAllocationIncrement string = "Increment"
- // GameServerAllocationIncrement is a Counter Action that indiciates the Counter's Count should be decremented at Allocation.
+ // GameServerAllocationDecrement is a Counter Action that indiciates the Counter's Count should be decremented at Allocation.
GameServerAllocationDecrement string = "Decrement"
)
diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go
index 4d791c224b..c300a3bd55 100644
--- a/pkg/apis/agones/v1/gameserver.go
+++ b/pkg/apis/agones/v1/gameserver.go
@@ -929,9 +929,11 @@ func (gs *GameServer) Patch(delta *GameServer) ([]byte, error) {
// UpdateCount increments or decrements a CounterStatus on a Game Server by the given amount.
func (gs *GameServer) UpdateCount(name string, action string, amount int64) error {
- err := fmt.Errorf("unable to UpdateCount: Name %s, Action %s, Amount %d", name, action, amount)
- if !(action == GameServerAllocationIncrement || action == GameServerAllocationDecrement) || amount < 0 {
- return err
+ if !(action == GameServerAllocationIncrement || action == GameServerAllocationDecrement) {
+ return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Allocation action must be one of %s or %s", name, action, amount, GameServerAllocationIncrement, GameServerAllocationDecrement)
+ }
+ if amount < 0 {
+ return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Amount must be greater than 0", name, action, amount)
}
if counter, ok := gs.Status.Counters[name]; ok {
cnt := counter.Count
@@ -939,82 +941,87 @@ func (gs *GameServer) UpdateCount(name string, action string, amount int64) erro
cnt += amount
// only check for Count > Capacity when incrementing
if cnt > counter.Capacity {
- return err
+ return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Incremented Count %d is greater than available capacity %d", name, action, amount, cnt, counter.Capacity)
}
- }
- if action == GameServerAllocationDecrement {
+ } else {
cnt -= amount
- }
- if cnt < 0 {
- return err
+ // only check for Count < 0 when decrementing
+ if cnt < 0 {
+ return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Decremented Count %d is less than 0", name, action, amount, cnt)
+ }
}
counter.Count = cnt
gs.Status.Counters[name] = counter
return nil
}
- return err
+ return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Counter not found in GameServer %s", name, action, amount, gs.ObjectMeta.GetName())
}
// UpdateCounterCapacity updates the CounterStatus Capacity to the given capacity.
func (gs *GameServer) UpdateCounterCapacity(name string, capacity int64) error {
- err := fmt.Errorf("unable to UpdateCounterCapacity: Name %s, Capacity %d", name, capacity)
if capacity < 0 {
- return err
+ return errors.Errorf("unable to UpdateCounterCapacity: Name %s, Capacity %d. Capacity must be greater than or equal to 0", name, capacity)
}
if counter, ok := gs.Status.Counters[name]; ok {
counter.Capacity = capacity
gs.Status.Counters[name] = counter
return nil
}
- return err
+ return errors.Errorf("unable to UpdateCounterCapacity: Name %s, Capacity %d. Counter not found in GameServer %s", name, capacity, gs.ObjectMeta.GetName())
}
// UpdateListCapacity updates the ListStatus Capacity to the given capacity.
func (gs *GameServer) UpdateListCapacity(name string, capacity int64) error {
- err := fmt.Errorf("unable to UpdateListCapacity: Name %s, Capacity %d", name, capacity)
if capacity < 0 || capacity > 1000 {
- return err
+ return errors.Errorf("unable to UpdateListCapacity: Name %s, Capacity %d. Capacity must be between 0 and 1000, inclusive", name, capacity)
}
if list, ok := gs.Status.Lists[name]; ok {
list.Capacity = capacity
gs.Status.Lists[name] = list
return nil
}
- return err
+ return errors.Errorf("unable to UpdateListCapacity: Name %s, Capacity %d. List not found in GameServer %s", name, capacity, gs.ObjectMeta.GetName())
}
-// AppendListValues adds given the values to the ListStatus Values list. Any duplicates are silently
-// dropped.
+// AppendListValues adds unique values to the ListStatus Values list.
func (gs *GameServer) AppendListValues(name string, values []string) error {
- err := fmt.Errorf("unable to AppendListValues: Name %s, Values %s", name, values)
if len(values) == 0 {
- return err
+ return errors.Errorf("unable to AppendListValues: Name %s, Values %s. No values to append", name, values)
}
if list, ok := gs.Status.Lists[name]; ok {
- vals := []string{}
- // Maintain ordering so new values are appended to the end of list.Values
- vals = append(vals, list.Values...)
- vals = append(vals, values...)
- vals = removeDuplicates(vals)
- if (len(vals) > int(list.Capacity)) || (len(vals) == len(list.Values)) {
- return err
- }
- list.Values = vals
+ mergedList := mergeRemoveDuplicates(list.Values, values)
+ if len(mergedList) > int(list.Capacity) {
+ return errors.Errorf("unable to AppendListValues: Name %s, Values %s. Appended list length %d exceeds list capacity %d", name, values, len(mergedList), list.Capacity)
+ }
+ // If all given values are duplicates we give an error warning.
+ if len(mergedList) == len(list.Values) {
+ return errors.Errorf("unable to AppendListValues: Name %s, Values %s. All appended values are duplicates of the existing list", name, values)
+ }
+ // If only some values are duplicates, those duplicate values are silently dropped.
+ list.Values = mergedList
gs.Status.Lists[name] = list
return nil
}
- return err
+ return errors.Errorf("unable to AppendListValues: Name %s, Values %s. List not found in GameServer %s", name, values, gs.ObjectMeta.GetName())
}
-// removeDuplicates removes any duplicate values from a list. Returns new list with unique values only.
-func removeDuplicates(values []string) []string {
+// mergeRemoveDuplicates merges two lists and removes any duplicate values.
+// Maintains ordering, so new values from list2 are appended to the end of list1.
+// Returns a new list with unique values only.
+func mergeRemoveDuplicates(list1 []string, list2 []string) []string {
+ uniqueList := []string{}
listMap := make(map[string]bool)
- uniqueValues := []string{}
- for _, v := range values {
- if _, ok := listMap[v]; !ok {
- uniqueValues = append(uniqueValues, v)
- listMap[v] = true
+ for _, v1 := range list1 {
+ if _, ok := listMap[v1]; !ok {
+ uniqueList = append(uniqueList, v1)
+ listMap[v1] = true
+ }
+ }
+ for _, v2 := range list2 {
+ if _, ok := listMap[v2]; !ok {
+ uniqueList = append(uniqueList, v2)
+ listMap[v2] = true
}
}
- return uniqueValues
+ return uniqueList
}
diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go
index 0f3fedcb9d..a6cec05d15 100644
--- a/pkg/apis/agones/v1/gameserver_test.go
+++ b/pkg/apis/agones/v1/gameserver_test.go
@@ -2050,34 +2050,39 @@ func TestGameServerAppendListValues(t *testing.T) {
}
}
-func TestRemoveDuplicates(t *testing.T) {
+func TestMergeRemoveDuplicates(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
- values []string
- want []string
+ str1 []string
+ str2 []string
+ want []string
}{
- "empty string array": {
- values: []string{},
- want: []string{},
+ "empty string arrays": {
+ str1: []string{},
+ str2: []string{},
+ want: []string{},
},
"no duplicates": {
- values: []string{"one", "two", "three"},
- want: []string{"one", "two", "three"},
+ str1: []string{"one"},
+ str2: []string{"two", "three"},
+ want: []string{"one", "two", "three"},
},
"remove one duplicate": {
- values: []string{"one", "one", "one"},
- want: []string{"one"},
+ str1: []string{"one", "one", "one"},
+ str2: []string{"one", "one", "one"},
+ want: []string{"one"},
},
"remove multiple duplicates": {
- values: []string{"one", "two", "one", "one", "two"},
- want: []string{"one", "two"},
+ str1: []string{"one", "two"},
+ str2: []string{"two", "one"},
+ want: []string{"one", "two"},
},
}
for test, testCase := range testCases {
t.Run(test, func(t *testing.T) {
- got := removeDuplicates(testCase.values)
+ got := mergeRemoveDuplicates(testCase.str1, testCase.str2)
assert.Equal(t, testCase.want, got)
})
}
diff --git a/pkg/apis/allocation/v1/gameserverallocation.go b/pkg/apis/allocation/v1/gameserverallocation.go
index 279ad2b789..04ca4d294d 100644
--- a/pkg/apis/allocation/v1/gameserverallocation.go
+++ b/pkg/apis/allocation/v1/gameserverallocation.go
@@ -15,6 +15,7 @@
package v1
import (
+ "errors"
"fmt"
"agones.dev/agones/pkg/apis"
@@ -295,41 +296,41 @@ func (s *GameServerSelector) matchCounters(gs *agonesv1.GameServer) bool {
}
// CounterActions attempts to peform any actions from the CounterAction on the GameServer Counter.
-// Returns a string list of any actions that could not be performed.
-func (ca *CounterAction) CounterActions(counter string, gs *agonesv1.GameServer) []string {
- errors := []string{}
+// Returns the errors of any actions that could not be performed.
+func (ca *CounterAction) CounterActions(counter string, gs *agonesv1.GameServer) error {
+ var errs error
if ca.Capacity != nil {
capErr := gs.UpdateCounterCapacity(counter, *ca.Capacity)
if capErr != nil {
- errors = append(errors, capErr.Error())
+ errs = errors.Join(errs, capErr)
}
}
if ca.Action != nil && ca.Amount != nil {
cntErr := gs.UpdateCount(counter, *ca.Action, *ca.Amount)
if cntErr != nil {
- errors = append(errors, cntErr.Error())
+ errs = errors.Join(errs, cntErr)
}
}
- return errors
+ return errs
}
// ListActions attempts to peform any actions from the ListAction on the GameServer List.
// Returns a string list of any actions that could not be performed.
-func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) []string {
- errors := []string{}
+func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) error {
+ var errs error
if la.Capacity != nil {
capErr := gs.UpdateListCapacity(list, *la.Capacity)
if capErr != nil {
- errors = append(errors, capErr.Error())
+ errs = errors.Join(errs, capErr)
}
}
if la.AddValues != nil && len(la.AddValues) > 0 {
cntErr := gs.AppendListValues(list, la.AddValues)
if cntErr != nil {
- errors = append(errors, cntErr.Error())
+ errs = errors.Join(errs, cntErr)
}
}
- return errors
+ return errs
}
// matchLists returns true if there is a match for the ListSelector in the GameServerStatus
diff --git a/pkg/apis/allocation/v1/gameserverallocation_test.go b/pkg/apis/allocation/v1/gameserverallocation_test.go
index 8a0d81d93c..fdf754917f 100644
--- a/pkg/apis/allocation/v1/gameserverallocation_test.go
+++ b/pkg/apis/allocation/v1/gameserverallocation_test.go
@@ -1032,11 +1032,11 @@ func TestGameServerCounterActions(t *testing.T) {
INCREMENT := "Increment"
testScenarios := map[string]struct {
- ca CounterAction
- counter string
- gs *agonesv1.GameServer
- want *agonesv1.GameServer
- errLength int
+ ca CounterAction
+ counter string
+ gs *agonesv1.GameServer
+ want *agonesv1.GameServer
+ wantErr bool
}{
"update counter capacity": {
ca: CounterAction{
@@ -1055,7 +1055,7 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 1,
Capacity: 0,
}}}},
- errLength: 0,
+ wantErr: false,
},
"fail update counter capacity and count": {
ca: CounterAction{
@@ -1076,7 +1076,7 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 99,
Capacity: 100,
}}}},
- errLength: 2,
+ wantErr: true,
},
"update counter count": {
ca: CounterAction{
@@ -1096,7 +1096,7 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 11,
Capacity: 100,
}}}},
- errLength: 0,
+ wantErr: false,
},
"update counter count and capacity": {
ca: CounterAction{
@@ -1117,15 +1117,19 @@ func TestGameServerCounterActions(t *testing.T) {
Count: 1,
Capacity: 10,
}}}},
- errLength: 0,
+ wantErr: false,
},
}
for test, testScenario := range testScenarios {
t.Run(test, func(t *testing.T) {
errs := testScenario.ca.CounterActions(testScenario.counter, testScenario.gs)
+ if errs != nil {
+ assert.True(t, testScenario.wantErr)
+ } else {
+ assert.False(t, testScenario.wantErr)
+ }
assert.Equal(t, testScenario.want, testScenario.gs)
- assert.Equal(t, testScenario.errLength, len(errs))
})
}
}
@@ -1138,11 +1142,11 @@ func TestGameServerListActions(t *testing.T) {
assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
testScenarios := map[string]struct {
- la ListAction
- list string
- gs *agonesv1.GameServer
- want *agonesv1.GameServer
- errLength int
+ la ListAction
+ list string
+ gs *agonesv1.GameServer
+ want *agonesv1.GameServer
+ wantErr bool
}{
"update list capacity": {
la: ListAction{
@@ -1161,7 +1165,7 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"page1", "page2"},
Capacity: 0,
}}}},
- errLength: 0,
+ wantErr: false,
},
"update list values": {
la: ListAction{
@@ -1180,7 +1184,7 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"sage1", "sage2", "sage3"},
Capacity: 100,
}}}},
- errLength: 0,
+ wantErr: false,
},
"update list values and capacity": {
la: ListAction{
@@ -1200,7 +1204,7 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"magician1", "magician2", "magician3"},
Capacity: 42,
}}}},
- errLength: 0,
+ wantErr: false,
},
"update list values and capacity - value add fails": {
la: ListAction{
@@ -1220,15 +1224,19 @@ func TestGameServerListActions(t *testing.T) {
Values: []string{"fairy1", "fairy2"},
Capacity: 2,
}}}},
- errLength: 1,
+ wantErr: true,
},
}
for test, testScenario := range testScenarios {
t.Run(test, func(t *testing.T) {
errs := testScenario.la.ListActions(testScenario.list, testScenario.gs)
+ if errs != nil {
+ assert.True(t, testScenario.wantErr)
+ } else {
+ assert.False(t, testScenario.wantErr)
+ }
assert.Equal(t, testScenario.want, testScenario.gs)
- assert.Equal(t, testScenario.errLength, len(errs))
})
}
}
diff --git a/pkg/gameserverallocations/allocator.go b/pkg/gameserverallocations/allocator.go
index 69895f384d..7b906974f1 100644
--- a/pkg/gameserverallocations/allocator.go
+++ b/pkg/gameserverallocations/allocator.go
@@ -18,6 +18,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
+ goErrors "errors"
"fmt"
"net/http"
"strings"
@@ -609,22 +610,24 @@ func (c *Allocator) applyAllocationToGameServer(ctx context.Context, mp allocati
// perfom any Counter or List actions
if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
- actionErrors := []string{}
+ var counterErrors error
+ var listErrors error
if gsa.Spec.Counters != nil {
for counter, ca := range gsa.Spec.Counters {
- errs := ca.CounterActions(counter, gs)
- actionErrors = append(actionErrors, errs...)
+ counterErrors = goErrors.Join(counterErrors, ca.CounterActions(counter, gs))
}
}
if gsa.Spec.Lists != nil {
for list, la := range gsa.Spec.Lists {
- errs := la.ListActions(list, gs)
- actionErrors = append(actionErrors, errs...)
+ listErrors = goErrors.Join(listErrors, la.ListActions(list, gs))
}
}
// record any Counter or List action errors as a warning
- if len(actionErrors) != 0 {
- c.recorder.Event(gs, corev1.EventTypeWarning, string(runtime.FeatureCountsAndLists), fmt.Sprint(actionErrors))
+ if counterErrors != nil {
+ c.recorder.Event(gs, corev1.EventTypeWarning, "Counter Errors", counterErrors.Error())
+ }
+ if listErrors != nil {
+ c.recorder.Event(gs, corev1.EventTypeWarning, "List Errors", listErrors.Error())
}
}
From 6296d8f5bc34d749d3dcf0f295b5c2431cc38638 Mon Sep 17 00:00:00 2001
From: Ivy Gooch
Date: Fri, 5 May 2023 19:12:01 +0000
Subject: [PATCH 7/7] Move event recording after successful game server update
---
pkg/gameserverallocations/allocator.go | 28 ++++++++++++++++----------
1 file changed, 17 insertions(+), 11 deletions(-)
diff --git a/pkg/gameserverallocations/allocator.go b/pkg/gameserverallocations/allocator.go
index 7b906974f1..58b8703a35 100644
--- a/pkg/gameserverallocations/allocator.go
+++ b/pkg/gameserverallocations/allocator.go
@@ -565,7 +565,6 @@ func (c *Allocator) allocationUpdateWorkers(ctx context.Context, workerCount int
res.err = errors.Wrap(err, "error updating allocated gameserver")
} else {
res.gs = gs
- c.recorder.Event(res.gs, corev1.EventTypeNormal, string(res.gs.Status.State), "Allocated")
}
res.request.response <- res
@@ -609,9 +608,9 @@ func (c *Allocator) applyAllocationToGameServer(ctx context.Context, mp allocati
gs.Status.State = agonesv1.GameServerStateAllocated
// perfom any Counter or List actions
+ var counterErrors error
+ var listErrors error
if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
- var counterErrors error
- var listErrors error
if gsa.Spec.Counters != nil {
for counter, ca := range gsa.Spec.Counters {
counterErrors = goErrors.Join(counterErrors, ca.CounterActions(counter, gs))
@@ -622,16 +621,23 @@ func (c *Allocator) applyAllocationToGameServer(ctx context.Context, mp allocati
listErrors = goErrors.Join(listErrors, la.ListActions(list, gs))
}
}
- // record any Counter or List action errors as a warning
- if counterErrors != nil {
- c.recorder.Event(gs, corev1.EventTypeWarning, "Counter Errors", counterErrors.Error())
- }
- if listErrors != nil {
- c.recorder.Event(gs, corev1.EventTypeWarning, "List Errors", listErrors.Error())
- }
}
- return c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gs, metav1.UpdateOptions{})
+ gsUpdate, updateErr := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gs, metav1.UpdateOptions{})
+ if updateErr != nil {
+ return gsUpdate, updateErr
+ }
+
+ // If successful Update record any Counter or List action errors as a warning
+ if counterErrors != nil {
+ c.recorder.Event(gsUpdate, corev1.EventTypeWarning, "CounterActionError", counterErrors.Error())
+ }
+ if listErrors != nil {
+ c.recorder.Event(gsUpdate, corev1.EventTypeWarning, "ListActionError", listErrors.Error())
+ }
+ c.recorder.Event(gsUpdate, corev1.EventTypeNormal, string(gsUpdate.Status.State), "Allocated")
+
+ return gsUpdate, updateErr
}
// Retry retries fn based on backoff provided.