Skip to content

Commit

Permalink
adding unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dgkanatsios committed Apr 18, 2022
1 parent c317e97 commit 6c07ea9
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 83 deletions.
68 changes: 2 additions & 66 deletions cmd/nodeagent/nodeagentmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"sync"
"time"

mpsv1alpha1 "github.com/playfab/thundernetes/pkg/operator/api/v1alpha1"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -35,20 +32,6 @@ const (
ErrHealthNotExists = "health does not exist"
)

var (
GameServerStates = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "thundernetes",
Name: "gameserver_states",
Help: "Game server states",
}, []string{"name", "state"})

ConnectedPlayersGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "thundernetes",
Name: "connected_players",
Help: "Number of connected players per GameServer",
}, []string{"namespace", "name"})
)

// NodeAgentManager manages the GameServer CRs that reside on this Node
// these game server process heartbeat to the NodeAgent process
// There is a two way communication between the game server and the NodeAgent
Expand Down Expand Up @@ -138,7 +121,7 @@ func (n *NodeAgentManager) gameServerCreatedOrUpdated(obj *unstructured.Unstruct
n.gameServerMap.Store(gameServerName, gsdi)
}

gameServerState, _, err := n.parseStateHealth(obj)
gameServerState, _, err := parseStateHealth(obj)
if err != nil {
logger.Warnf("parsing state/health: %s. This is OK if the GameServer was just created", err.Error())
}
Expand All @@ -149,7 +132,7 @@ func (n *NodeAgentManager) gameServerCreatedOrUpdated(obj *unstructured.Unstruct
}

// server is Active, so get session details as well initial players details
sessionID, sessionCookie, initialPlayers := n.parseSessionDetails(obj, gameServerName, gameServerNamespace)
sessionID, sessionCookie, initialPlayers := parseSessionDetails(obj, gameServerName, gameServerNamespace)
logger.Infof("getting values from allocation - GameServer CR, sessionID:%s, sessionCookie:%s, initialPlayers: %v", sessionID, sessionCookie, initialPlayers)

// create the GameServerDetails CR
Expand Down Expand Up @@ -416,53 +399,6 @@ func (n *NodeAgentManager) updateConnectedPlayersIfNeeded(ctx context.Context, h
return nil
}

// parseSessionDetails returns the sessionID and sessionCookie from the unstructured GameServer CR
func (n *NodeAgentManager) parseSessionDetails(u *unstructured.Unstructured, gameServerName, gameServerNamespace string) (string, string, []string) {
logger := getLogger(gameServerName, gameServerNamespace)
sessionID, sessionIDExists, sessionIDErr := unstructured.NestedString(u.Object, "status", "sessionID")
sessionCookie, sessionCookieExists, sessionCookieErr := unstructured.NestedString(u.Object, "status", "sessionCookie")
initialPlayers, initialPlayersExists, initialPlayersErr := unstructured.NestedStringSlice(u.Object, "status", "initialPlayers")

if !sessionIDExists || !sessionCookieExists || !initialPlayersExists {
logger.Debugf("sessionID or sessionCookie or initialPlayers do not exist, sessionIDExists: %t, sessionCookieExists: %t, initialPlayersExists: %t", sessionIDExists, sessionCookieExists, initialPlayersExists)
}

if sessionIDErr != nil {
logger.Debugf("error getting sessionID: %s", sessionIDErr.Error())
}

if sessionCookieErr != nil {
logger.Debugf("error getting sessionCookie: %s", sessionCookieErr.Error())
}

if initialPlayersErr != nil {
logger.Debugf("error getting initialPlayers: %s", initialPlayersErr.Error())
}

return sessionID, sessionCookie, initialPlayers
}

// parseState parses the GameServer state from the unstructured GameServer CR
func (n *NodeAgentManager) parseStateHealth(u *unstructured.Unstructured) (string, string, error) {
state, stateExists, stateErr := unstructured.NestedString(u.Object, "status", "state")
health, healthExists, healthErr := unstructured.NestedString(u.Object, "status", "health")

if stateErr != nil {
return "", "", stateErr
}
if !stateExists {
return "", "", errors.New(ErrStateNotExists)
}

if healthErr != nil {
return "", "", stateErr
}
if !healthExists {
return "", "", errors.New(ErrHealthNotExists)
}
return state, health, nil
}

// createGameServerDetails creates a GameServerDetails CR with the specified name and namespace
func (n *NodeAgentManager) createGameServerDetails(ctx context.Context, gsuid types.UID, gsname, gsnamespace string, connectedPlayers []string) error {
gs := &mpsv1alpha1.GameServer{
Expand Down
201 changes: 201 additions & 0 deletions cmd/nodeagent/nodeagentmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -167,6 +168,188 @@ var _ = Describe("nodeagent tests", func() {
Expect(err).ToNot(HaveOccurred())
Expect(hbr.Operation).To(Equal(GameOperationContinue))
})
It("should not create a GameServerDetail if the server is not Active", func() {
dynamicClient := newDynamicInterface()

n := NewNodeAgentManager(dynamicClient, testNodeName, false)
gs := createUnstructuredTestGameServer(testGameServerName, testGameServerNamespace)

_, err := dynamicClient.Resource(gameserverGVR).Namespace(testGameServerNamespace).Create(context.Background(), gs, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())

// wait for the create trigger on the watch
Eventually(func() bool {
var ok bool
_, ok = n.gameServerMap.Load(testGameServerName)
return ok
}).Should(BeTrue())

// simulate 5 standingBy heartbeats
for i := 0; i < 5; i++ {
hb := &HeartbeatRequest{
CurrentGameState: GameStateStandingBy,
CurrentGameHealth: "Healthy",
}
b, _ := json.Marshal(hb)
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/v1/sessionHosts/%s", testGameServerName), bytes.NewReader(b))
w := httptest.NewRecorder()

n.heartbeatHandler(w, req)
res := w.Result()
defer res.Body.Close()
Expect(res.StatusCode).To(Equal(http.StatusOK))
resBody, err := ioutil.ReadAll(res.Body)
Expect(err).ToNot(HaveOccurred())
hbr := HeartbeatResponse{}
_ = json.Unmarshal(resBody, &hbr)
Expect(hbr.Operation).To(Equal(GameOperationContinue))
}

_, err = dynamicClient.Resource(gameserverDetailGVR).Namespace(testGameServerNamespace).Get(context.Background(), gs.GetName(), metav1.GetOptions{})
Expect(err).To(HaveOccurred())
Expect(apierrors.IsNotFound(err)).To(BeTrue())

})
It("should delete the GameServer from the cache when it's delete on K8s", func() {
dynamicClient := newDynamicInterface()

n := NewNodeAgentManager(dynamicClient, testNodeName, false)
gs := createUnstructuredTestGameServer(testGameServerName, testGameServerNamespace)

_, err := dynamicClient.Resource(gameserverGVR).Namespace(testGameServerNamespace).Create(context.Background(), gs, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())

// wait for the create trigger on the watch
Eventually(func() bool {
var ok bool
_, ok = n.gameServerMap.Load(testGameServerName)
return ok
}).Should(BeTrue())

err = dynamicClient.Resource(gameserverGVR).Namespace(testGameServerNamespace).Delete(context.Background(), gs.GetName(), metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())

Eventually(func() bool {
var ok bool
_, ok = n.gameServerMap.Load(testGameServerName)
return ok
}).Should(BeTrue())
})
It("should create a GameServerDetail on subsequent heartbeats, if it fails on the first time", func() {
dynamicClient := newDynamicInterface()

n := NewNodeAgentManager(dynamicClient, testNodeName, false)
gs := createUnstructuredTestGameServer(testGameServerName, testGameServerNamespace)

_, err := dynamicClient.Resource(gameserverGVR).Namespace(testGameServerNamespace).Create(context.Background(), gs, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())

// wait for the create trigger on the watch
var gsinfo interface{}
Eventually(func() bool {
var ok bool
gsinfo, ok = n.gameServerMap.Load(testGameServerName)
return ok
}).Should(BeTrue())

// simulate subsequent updates by GSDK
gsinfo.(*GameServerInfo).PreviousGameState = GameStateStandingBy
gsinfo.(*GameServerInfo).PreviousGameHealth = "Healthy"

// update GameServer CR to active
gs.Object["status"].(map[string]interface{})["state"] = "Active"
gs.Object["status"].(map[string]interface{})["health"] = "Healthy"
_, err = dynamicClient.Resource(gameserverGVR).Namespace(testGameServerNamespace).Update(context.Background(), gs, metav1.UpdateOptions{})
Expect(err).ToNot(HaveOccurred())

// wait for the update trigger on the watch
Eventually(func() bool {
tempgs, ok := n.gameServerMap.Load(testGameServerName)
if !ok {
return false
}
tempgs.(*GameServerInfo).Mutex.RLock()
gsd := *tempgs.(*GameServerInfo)
tempgs.(*GameServerInfo).Mutex.RUnlock()
return gsd.IsActive && gsd.PreviousGameState == GameStateStandingBy
}).Should(BeTrue())

// wait till the GameServerDetail CR has been created
Eventually(func(g Gomega) {
u, err := dynamicClient.Resource(gameserverDetailGVR).Namespace(testGameServerNamespace).Get(context.Background(), gs.GetName(), metav1.GetOptions{})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(u.GetName()).To(Equal(gs.GetName()))
}).Should(Succeed())

// delete the GameServerDetail CR to simulate failure in creating
Eventually(func(g Gomega) {
err := dynamicClient.Resource(gameserverDetailGVR).Namespace(testGameServerNamespace).Delete(context.Background(), gs.GetName(), metav1.DeleteOptions{})
g.Expect(err).ToNot(HaveOccurred())
}).Should(Succeed())

// heartbeat from the game is still StandingBy
hb := &HeartbeatRequest{
CurrentGameState: GameStateStandingBy,
CurrentGameHealth: "Healthy",
}
b, _ := json.Marshal(hb)
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/v1/sessionHosts/%s", testGameServerName), bytes.NewReader(b))
w := httptest.NewRecorder()

// but the response from NodeAgent should be active
n.heartbeatHandler(w, req)
res := w.Result()
defer res.Body.Close()
Expect(res.StatusCode).To(Equal(http.StatusOK))
resBody, err := ioutil.ReadAll(res.Body)
Expect(err).ToNot(HaveOccurred())
hbr := HeartbeatResponse{}
_ = json.Unmarshal(resBody, &hbr)
Expect(hbr.Operation).To(Equal(GameOperationActive))

// next heartbeat response should be continue
// at the same time, game is adding some connected players
hb = &HeartbeatRequest{
CurrentGameState: GameStateActive, // heartbeat is now active
CurrentGameHealth: "Healthy",
CurrentPlayers: getTestConnectedPlayers(), // adding connected players
}
b, _ = json.Marshal(hb)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/v1/sessionHosts/%s", testGameServerName), bytes.NewReader(b))
w = httptest.NewRecorder()
n.heartbeatHandler(w, req)
res = w.Result()
defer res.Body.Close()
// first heartbeat after Active should fail since the GameServerDetail is missing
Expect(res.StatusCode).To(Equal(http.StatusInternalServerError))

// next heartbeat should succeed
hb = &HeartbeatRequest{
CurrentGameState: GameStateActive, // heartbeat is now active
CurrentGameHealth: "Healthy",
CurrentPlayers: getTestConnectedPlayers(),
}
b, _ = json.Marshal(hb)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/v1/sessionHosts/%s", testGameServerName), bytes.NewReader(b))
w = httptest.NewRecorder()
n.heartbeatHandler(w, req)
res = w.Result()
defer res.Body.Close()
Expect(res.StatusCode).To(Equal(http.StatusOK))
resBody, err = ioutil.ReadAll(res.Body)
Expect(err).ToNot(HaveOccurred())
hbr = HeartbeatResponse{}
err = json.Unmarshal(resBody, &hbr)
Expect(err).ToNot(HaveOccurred())
Expect(hbr.Operation).To(Equal(GameOperationContinue))

// make sure the GameServerDetail CR has been created
Eventually(func(g Gomega) {
u, err := dynamicClient.Resource(gameserverDetailGVR).Namespace(testGameServerNamespace).Get(context.Background(), gs.GetName(), metav1.GetOptions{})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(u.GetName()).To(Equal(gs.GetName()))
}).Should(Succeed())
})
It("should handle a lot of simultaneous heartbeats from different game servers", func() {
rand.Seed(time.Now().UnixNano())

Expand Down Expand Up @@ -216,6 +399,13 @@ var _ = Describe("nodeagent tests", func() {
return gsd.IsActive && tempgs.(*GameServerInfo).PreviousGameState == GameStateStandingBy
}).Should(BeTrue())

// wait till the GameServerDetail CR has been created
Eventually(func(g Gomega) {
u, err := dynamicClient.Resource(gameserverDetailGVR).Namespace(testGameServerNamespace).Get(context.Background(), gs.GetName(), metav1.GetOptions{})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(u.GetName()).To(Equal(gs.GetName()))
}).Should(Succeed())

// heartbeat from the game is still StandingBy
hb := &HeartbeatRequest{
CurrentGameState: GameStateStandingBy,
Expand Down Expand Up @@ -299,6 +489,17 @@ func createUnstructuredTestGameServer(name, namespace string) *unstructured.Unst
return &unstructured.Unstructured{Object: g}
}

func getTestConnectedPlayers() []ConnectedPlayer {
return []ConnectedPlayer{
{
PlayerId: "player1",
},
{
PlayerId: "player2",
},
}
}

func TestNodeAgent(t *testing.T) {
RegisterFailHandler(Fail)

Expand Down
16 changes: 16 additions & 0 deletions cmd/nodeagent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"sync"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
)
Expand Down Expand Up @@ -44,6 +46,20 @@ const (
GameOperationTerminate GameOperation = "Terminate"
)

var (
GameServerStates = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "thundernetes",
Name: "gameserver_states",
Help: "Game server states",
}, []string{"name", "state"})

ConnectedPlayersGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "thundernetes",
Name: "connected_players",
Help: "Number of connected players per GameServer",
}, []string{"namespace", "name"})
)

// HeartbeatRequest contains data for the heartbeat request coming from the GSDK running alongside GameServer
type HeartbeatRequest struct {
// CurrentGameState is the current state of the game server
Expand Down
Loading

0 comments on commit 6c07ea9

Please sign in to comment.