Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of Local SDK Server Player Tracking #1496

Merged
merged 1 commit into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 123 additions & 16 deletions pkg/sdkserver/localsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"math/rand"
"os"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -197,6 +198,8 @@ func (l *LocalSDKServer) recordRequestWithValue(request string, value string, ob
fieldVal = l.gs.ObjectMeta.Uid
case "PlayerCapacity":
fieldVal = strconv.FormatInt(l.gs.Status.Players.Capacity, 10)
case "PlayerIDs":
fieldVal = strings.Join(l.gs.Status.Players.IDs, ",")
default:
l.logger.Error("unexpected Field to compare")
}
Expand Down Expand Up @@ -384,28 +387,139 @@ func (l *LocalSDKServer) stopReserveTimer() {
// [Stage:Alpha]
// [FeatureFlag:PlayerTracking]
func (l *LocalSDKServer) PlayerConnect(ctx context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
panic("implement me")
if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled")
}
l.logger.WithField("playerID", id.PlayerID).Info("Player Connected")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()

if l.gs.Status.Players == nil {
l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{}
}

// the player is already connected, return false.
for _, playerID := range l.gs.Status.Players.IDs {
if playerID == id.PlayerID {
return &alpha.Bool{Bool: false}, nil
}
}

if l.gs.Status.Players.Count >= l.gs.Status.Players.Capacity {
return &alpha.Bool{}, errors.New("Players are already at capacity")
}

l.gs.Status.Players.IDs = append(l.gs.Status.Players.IDs, id.PlayerID)
l.gs.Status.Players.Count = int64(len(l.gs.Status.Players.IDs))

l.update <- struct{}{}
l.recordRequestWithValue("playerconnect", "1234", "PlayerIDs")
return &alpha.Bool{Bool: true}, nil
}

// PlayerDisconnect should be called when a player disconnects.
// [Stage:Alpha]
// [FeatureFlag:PlayerTracking]
func (l *LocalSDKServer) PlayerDisconnect(ctx context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
panic("implement me")
if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled")
}
l.logger.WithField("playerID", id.PlayerID).Info("Player Disconnected")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()

if l.gs.Status.Players == nil {
l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{}
}

found := -1
for i, playerID := range l.gs.Status.Players.IDs {
if playerID == id.PlayerID {
found = i
break
}
}
if found == -1 {
return &alpha.Bool{Bool: false}, nil
}

l.gs.Status.Players.IDs = append(l.gs.Status.Players.IDs[:found], l.gs.Status.Players.IDs[found+1:]...)
l.gs.Status.Players.Count = int64(len(l.gs.Status.Players.IDs))

l.update <- struct{}{}
l.recordRequestWithValue("playerdisconnect", "", "PlayerIDs")
return &alpha.Bool{Bool: true}, nil
}

// IsPlayerConnected returns if the playerID is currently connected to the GameServer.
// [Stage:Alpha]
// [FeatureFlag:PlayerTracking]
func (l *LocalSDKServer) IsPlayerConnected(c context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled")
}

result := &alpha.Bool{Bool: false}
l.logger.WithField("playerID", id.PlayerID).Info("Is a Player Connected?")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()

l.recordRequestWithValue("isplayerconnected", id.PlayerID, "PlayerIDs")

if l.gs.Status.Players == nil {
return result, nil
}

for _, playerID := range l.gs.Status.Players.IDs {
if id.PlayerID == playerID {
result.Bool = true
break
}
}

return result, nil
}

// IsPlayerConnected returns if the player ID is connected or not
// GetConnectedPlayers returns the list of the currently connected player ids.
// [Stage:Alpha]
// [FeatureFlag:PlayerTracking]
func (l *LocalSDKServer) IsPlayerConnected(ctx context.Context, id *alpha.PlayerID) (*alpha.Bool, error) {
panic("implement me")
func (l *LocalSDKServer) GetConnectedPlayers(c context.Context, empty *alpha.Empty) (*alpha.PlayerIDList, error) {
if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled")
}
l.logger.Info("Getting Connected Players")

result := &alpha.PlayerIDList{List: []string{}}

l.gsMutex.Lock()
defer l.gsMutex.Unlock()
l.recordRequest("getconnectedplayers")

if l.gs.Status.Players == nil {
return result, nil
}
result.List = l.gs.Status.Players.IDs
return result, nil
}

// GetConnectedPlayers returns if the players are connected or not
// GetPlayerCount returns the current player count.
// [Stage:Alpha]
// [FeatureFlag:PlayerTracking]
func (l *LocalSDKServer) GetConnectedPlayers(ctx context.Context, empty *alpha.Empty) (*alpha.PlayerIDList, error) {
panic("implement me")
func (l *LocalSDKServer) GetPlayerCount(ctx context.Context, _ *alpha.Empty) (*alpha.Count, error) {
if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) {
return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled")
}
l.logger.Info("Getting Player Count")
l.recordRequest("getplayercount")
l.gsMutex.RLock()
defer l.gsMutex.RUnlock()

result := &alpha.Count{}
if l.gs.Status.Players != nil {
result.Count = l.gs.Status.Players.Count
}

return result, nil
}

// SetPlayerCapacity to change the game server's player capacity.
Expand Down Expand Up @@ -454,13 +568,6 @@ func (l *LocalSDKServer) GetPlayerCapacity(_ context.Context, _ *alpha.Empty) (*
return result, nil
}

// GetPlayerCount returns the current player count.
// [Stage:Alpha]
// [FeatureFlag:PlayerTracking]
func (l *LocalSDKServer) GetPlayerCount(ctx context.Context, _ *alpha.Empty) (*alpha.Count, error) {
panic("implement me")
}

// Close tears down all the things
func (l *LocalSDKServer) Close() {
l.updateObservers.Range(func(observer, _ interface{}) bool {
Expand Down Expand Up @@ -498,7 +605,7 @@ func (l *LocalSDKServer) EqualSets(expected, received []string) bool {
func (l *LocalSDKServer) compare() {
if l.testMode {
if !l.EqualSets(l.expectedSequence, l.requestSequence) {
l.logger.Errorf("Testing Failed %v %v", l.expectedSequence, l.requestSequence)
l.logger.WithField("expected", l.expectedSequence).WithField("received", l.requestSequence).Info("Testing Failed")
os.Exit(1)
} else {
l.logger.Info("Received requests match expected list. Test run was successful")
Expand Down
130 changes: 129 additions & 1 deletion pkg/sdkserver/localsdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,134 @@ func TestLocalSDKServerPlayerCapacity(t *testing.T) {
assert.Equal(t, int64(10), gs.Status.Players.Capacity)
}

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

runtime.FeatureTestMutex.Lock()
defer runtime.FeatureTestMutex.Unlock()
assert.NoError(t, runtime.ParseFeatures(string(runtime.FeaturePlayerTracking)+"=true"))

fixture := &agonesv1.GameServer{
ObjectMeta: metav1.ObjectMeta{Name: "stuff"},
Status: agonesv1.GameServerStatus{
Players: &agonesv1.PlayerStatus{
Capacity: 1,
},
},
}

e := &alpha.Empty{}
path, err := gsToTmpFile(fixture)
assert.NoError(t, err)
l, err := NewLocalSDKServer(path)
assert.Nil(t, err)

stream := newGameServerMockStream()
go func() {
err := l.WatchGameServer(&sdk.Empty{}, stream)
assert.Nil(t, err)
}()

// wait for watching to begin
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
found := false
l.updateObservers.Range(func(_, _ interface{}) bool {
found = true
return false
})
return found, nil
})
assert.NoError(t, err)

count, err := l.GetPlayerCount(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, int64(0), count.Count)

list, err := l.GetConnectedPlayers(context.Background(), e)
assert.NoError(t, err)
assert.Empty(t, list.List)

id := &alpha.PlayerID{PlayerID: "one"}
// connect a player
ok, err := l.PlayerConnect(context.Background(), id)
assert.NoError(t, err)
assert.True(t, ok.Bool, "Player should not exist yet")

count, err = l.GetPlayerCount(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, int64(1), count.Count)

expected := &sdk.GameServer_Status_PlayerStatus{
Count: 1,
Capacity: 1,
IDs: []string{id.PlayerID},
}
assertWatchUpdate(t, stream, expected, func(gs *sdk.GameServer) interface{} {
return gs.Status.Players
})

ok, err = l.IsPlayerConnected(context.Background(), id)
assert.NoError(t, err)
assert.True(t, ok.Bool, "player should be connected")

list, err = l.GetConnectedPlayers(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, []string{id.PlayerID}, list.List)

// add same player
ok, err = l.PlayerConnect(context.Background(), id)
assert.NoError(t, err)
assert.False(t, ok.Bool, "Player already exists")

count, err = l.GetPlayerCount(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, int64(1), count.Count)
assertNoWatchUpdate(t, stream)

list, err = l.GetConnectedPlayers(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, []string{id.PlayerID}, list.List)

// should return an error if we try to add another, since we're at capacity
nopePlayer := &alpha.PlayerID{PlayerID: "nope"}
_, err = l.PlayerConnect(context.Background(), nopePlayer)
assert.EqualError(t, err, "Players are already at capacity")

ok, err = l.IsPlayerConnected(context.Background(), nopePlayer)
assert.NoError(t, err)
assert.False(t, ok.Bool)

// disconnect a player
ok, err = l.PlayerDisconnect(context.Background(), id)
assert.NoError(t, err)
assert.True(t, ok.Bool, "Player should be removed")
count, err = l.GetPlayerCount(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, int64(0), count.Count)

expected = &sdk.GameServer_Status_PlayerStatus{
Count: 0,
Capacity: 1,
IDs: []string{},
}
assertWatchUpdate(t, stream, expected, func(gs *sdk.GameServer) interface{} {
return gs.Status.Players
})

list, err = l.GetConnectedPlayers(context.Background(), e)
assert.NoError(t, err)
assert.Empty(t, list.List)

// remove same player
ok, err = l.PlayerDisconnect(context.Background(), id)
assert.NoError(t, err)
assert.False(t, ok.Bool, "Player already be gone")
count, err = l.GetPlayerCount(context.Background(), e)
assert.NoError(t, err)
assert.Equal(t, int64(0), count.Count)
assertNoWatchUpdate(t, stream)
}

// TestLocalSDKServerStateUpdates verify that SDK functions changes the state of the
// GameServer object
func TestLocalSDKServerStateUpdates(t *testing.T) {
Expand Down Expand Up @@ -374,7 +502,7 @@ func TestSDKConformanceFunctionality(t *testing.T) {
setAnnotation := "setannotation"
l.gs.ObjectMeta.Uid = exampleUID

expected := []string{}
var expected []string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why this is better than before.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, my IDE gave me a hint, and I clicked it. It's nicer because it doesn't allocate space to an array that is empty when nil will work just the same -- but for a test it's relatively inconsequential.

I can remove it if you would prefer?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to change, I think it would be better this way.

expected = append(expected, "", setAnnotation)

wg := sync.WaitGroup{}
Expand Down