Skip to content

Commit

Permalink
Adding proper GS state handling for local SDK
Browse files Browse the repository at this point in the history
Update the state of local SDK GS according to calls which were made
by SDK client. Local sidecar now keeps the current state.
For googleforgames#958.
  • Loading branch information
aLekSer committed Aug 6, 2019
1 parent ddb166a commit 74e0d3f
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 23 deletions.
145 changes: 129 additions & 16 deletions pkg/sdkserver/localsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,64 @@ var (
// is being run for local development, and doesn't connect to the
// Kubernetes cluster
type LocalSDKServer struct {
gsMutex sync.RWMutex
gs *sdk.GameServer
update chan struct{}
updateObservers sync.Map
requestSequence []string
expectedSequence []string
testMode bool
gsMutex sync.RWMutex
gs *sdk.GameServer
update chan struct{}
updateObservers sync.Map
requestSequence []string
expectedSequence []string
gsState agonesv1.GameServerState
gsReserveDuration *time.Duration
reserveTimer *time.Timer
testMode bool
validStateTransitions map[string]bool
}

func insertStateTransition(m map[string]bool, from, to agonesv1.GameServerState) {
m[string(from)+string(to)] = true
}

func initValidStateTransitions() map[string]bool {
stateTransitions := make(map[string]bool)
insertStateTransition(stateTransitions, agonesv1.GameServerStatePortAllocation, agonesv1.GameServerStateCreating)
insertStateTransition(stateTransitions, agonesv1.GameServerStateCreating, agonesv1.GameServerStateStarting)
insertStateTransition(stateTransitions, agonesv1.GameServerStateStarting, agonesv1.GameServerStateScheduled)

insertStateTransition(stateTransitions, agonesv1.GameServerStateScheduled, agonesv1.GameServerStateRequestReady)
insertStateTransition(stateTransitions, agonesv1.GameServerStateScheduled, agonesv1.GameServerStateReserved)
insertStateTransition(stateTransitions, agonesv1.GameServerStateScheduled, agonesv1.GameServerStateShutdown)
insertStateTransition(stateTransitions, agonesv1.GameServerStateScheduled, agonesv1.GameServerStateUnhealthy)

insertStateTransition(stateTransitions, agonesv1.GameServerStateRequestReady, agonesv1.GameServerStateReady)
insertStateTransition(stateTransitions, agonesv1.GameServerStateRequestReady, agonesv1.GameServerStateShutdown)
insertStateTransition(stateTransitions, agonesv1.GameServerStateRequestReady, agonesv1.GameServerStateUnhealthy)

insertStateTransition(stateTransitions, agonesv1.GameServerStateReserved, agonesv1.GameServerStateAllocated)
insertStateTransition(stateTransitions, agonesv1.GameServerStateReserved, agonesv1.GameServerStateRequestReady)
insertStateTransition(stateTransitions, agonesv1.GameServerStateReserved, agonesv1.GameServerStateShutdown)
insertStateTransition(stateTransitions, agonesv1.GameServerStateReserved, agonesv1.GameServerStateUnhealthy)

insertStateTransition(stateTransitions, agonesv1.GameServerStateReady, agonesv1.GameServerStateAllocated)
insertStateTransition(stateTransitions, agonesv1.GameServerStateReady, agonesv1.GameServerStateShutdown)
insertStateTransition(stateTransitions, agonesv1.GameServerStateReady, agonesv1.GameServerStateUnhealthy)

insertStateTransition(stateTransitions, agonesv1.GameServerStateAllocated, agonesv1.GameServerStateShutdown)
insertStateTransition(stateTransitions, agonesv1.GameServerStateAllocated, agonesv1.GameServerStateUnhealthy)
return stateTransitions
}

// NewLocalSDKServer returns the default LocalSDKServer
func NewLocalSDKServer(filePath string) (*LocalSDKServer, error) {
stateTransitions := initValidStateTransitions()
l := &LocalSDKServer{
gsMutex: sync.RWMutex{},
gs: defaultGs,
update: make(chan struct{}),
updateObservers: sync.Map{},
requestSequence: make([]string, 0),
testMode: false,
gsMutex: sync.RWMutex{},
gs: defaultGs,
update: make(chan struct{}),
updateObservers: sync.Map{},
requestSequence: make([]string, 0),
testMode: false,
gsState: agonesv1.GameServerStateScheduled,
validStateTransitions: stateTransitions,
}

if filePath != "" {
Expand Down Expand Up @@ -123,6 +163,15 @@ func NewLocalSDKServer(filePath string) (*LocalSDKServer, error) {
return l, nil
}

// ValidTransition helper function to check the validity of a transition from one Step to Another
func (l *LocalSDKServer) ValidTransition(prevState, newState agonesv1.GameServerState) bool {
if prevState == newState {
return false
}

return l.validStateTransitions[string(prevState)+string(newState)]
}

// GenerateUID - generate gameserver UID at random for testing
func (l *LocalSDKServer) GenerateUID() {
// Generating Random UID
Expand Down Expand Up @@ -171,24 +220,58 @@ func (l *LocalSDKServer) recordRequestWithValue(request string, value string, ob
}
}

func (l *LocalSDKServer) updateState(newState agonesv1.GameServerState) bool {
if l.ValidTransition(l.gsState, newState) {
l.gsState = newState
l.gs.Status.State = string(l.gsState)
} else {
logrus.Errorf("Cannot update state from %s to %s: invalid transition", l.gsState, newState)
return false
}
return true
}

// Ready logs that the Ready request has been received
func (l *LocalSDKServer) Ready(context.Context, *sdk.Empty) (*sdk.Empty, error) {
logrus.Info("Ready request has been received!")
l.recordRequest("ready")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()

// Follow the GameServer state diagram
if l.updateState(agonesv1.GameServerStateRequestReady) {
l.update <- struct{}{}
}
if l.updateState(agonesv1.GameServerStateReady) {
l.stopReserveTimer()
l.update <- struct{}{}
}
return &sdk.Empty{}, nil
}

// Allocate logs that an allocate request has been received
func (l *LocalSDKServer) Allocate(context.Context, *sdk.Empty) (*sdk.Empty, error) {
logrus.Info("Allocate request has been received!")
l.recordRequest("allocate")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()
if l.updateState(agonesv1.GameServerStateAllocated) {
l.stopReserveTimer()
l.update <- struct{}{}
}
return &sdk.Empty{}, nil
}

// Shutdown logs that the shutdown request has been received
func (l *LocalSDKServer) Shutdown(context.Context, *sdk.Empty) (*sdk.Empty, error) {
logrus.Info("Shutdown request has been received!")
l.recordRequest("shutdown")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()
if l.updateState(agonesv1.GameServerStateShutdown) {
l.stopReserveTimer()
l.update <- struct{}{}
}
return &sdk.Empty{}, nil
}

Expand Down Expand Up @@ -246,7 +329,7 @@ func (l *LocalSDKServer) SetAnnotation(_ context.Context, kv *sdk.KeyValue) (*sd
return &sdk.Empty{}, nil
}

// GetGameServer returns a dummy game server.
// GetGameServer returns current GameServer configuration.
func (l *LocalSDKServer) GetGameServer(context.Context, *sdk.Empty) (*sdk.GameServer, error) {
logrus.Info("getting GameServer details")
l.recordRequest("gameserver")
Expand All @@ -255,7 +338,7 @@ func (l *LocalSDKServer) GetGameServer(context.Context, *sdk.Empty) (*sdk.GameSe
return l.gs, nil
}

// WatchGameServer will return a dummy GameServer (with no changes), 3 times, every 5 seconds
// WatchGameServer will return current GameServer configuration, 3 times, every 5 seconds
func (l *LocalSDKServer) WatchGameServer(_ *sdk.Empty, stream sdk.SDK_WatchGameServerServer) error {
logrus.Info("connected to watch GameServer...")
observer := make(chan struct{})
Expand All @@ -281,12 +364,42 @@ func (l *LocalSDKServer) WatchGameServer(_ *sdk.Empty, stream sdk.SDK_WatchGameS
}

// Reserve moves this GameServer to the Reserved state for the Duration specified
func (l *LocalSDKServer) Reserve(_ context.Context, d *sdk.Duration) (*sdk.Empty, error) {
func (l *LocalSDKServer) Reserve(ctx context.Context, d *sdk.Duration) (*sdk.Empty, error) {
logrus.WithField("duration", d).Info("Reserve request has been received!")
l.recordRequest("reserve")
l.gsMutex.Lock()
defer l.gsMutex.Unlock()
if d.Seconds > 0 {
duration := time.Duration(d.Seconds) * time.Second
l.gsReserveDuration = &duration
l.resetReserveAfter(ctx, *l.gsReserveDuration)
}

if l.updateState(agonesv1.GameServerStateReserved) {
l.update <- struct{}{}
}
return &sdk.Empty{}, nil
}

func (l *LocalSDKServer) resetReserveAfter(ctx context.Context, duration time.Duration) {
if l.reserveTimer != nil {
l.reserveTimer.Stop()
}

l.reserveTimer = time.AfterFunc(duration, func() {
if _, err := l.Ready(ctx, &sdk.Empty{}); err != nil {
logrus.WithError(err).Error("error returning to Ready after reserved ")
}
})
}

func (l *LocalSDKServer) stopReserveTimer() {
if l.reserveTimer != nil {
l.reserveTimer.Stop()
}
l.gsReserveDuration = nil
}

// Close tears down all the things
func (l *LocalSDKServer) Close() {
l.updateObservers.Range(func(observer, _ interface{}) bool {
Expand Down
45 changes: 45 additions & 0 deletions pkg/sdkserver/localsdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,48 @@ func gsToTmpFile(gs *agonesv1.GameServer) (string, error) {
err = json.NewEncoder(file).Encode(gs)
return file.Name(), err
}

func TestLocalSDKServerValidTransition(t *testing.T) {
t.Parallel()
l, err := NewLocalSDKServer("")
assert.Nil(t, err)
got := l.ValidTransition(agonesv1.GameServerStateScheduled, agonesv1.GameServerStateScheduled);
assert.False(t, got , "LocalSDKServer.ValidTransition()")
got = l.ValidTransition(agonesv1.GameServerStateScheduled, agonesv1.GameServerStateReserved);
assert.True(t, got , "LocalSDKServer.ValidTransition()")
got = l.ValidTransition(agonesv1.GameServerStateReserved, agonesv1.GameServerStateScheduled);
assert.False(t, got , "LocalSDKServer.ValidTransition()")

ctx := context.Background()
e := &sdk.Empty{}
_, err = l.Ready(ctx, e)
assert.Nil(t, err)

gs, err := l.GetGameServer(ctx, e)
assert.Nil(t, err)
assert.Equal(t, gs.Status.State, string(agonesv1.GameServerStateReady))

// Transition to Reserve from Ready Status is fobidden - State should left unchanged
seconds := &sdk.Duration{Seconds: 2}
_, err = l.Reserve(ctx, seconds)
assert.Nil(t, err)

gs, err = l.GetGameServer(ctx, e)
assert.Nil(t, err)
assert.Equal(t, gs.Status.State, string(agonesv1.GameServerStateReady))

_, err = l.Allocate(ctx, e)
assert.Nil(t, err)

gs, err = l.GetGameServer(ctx, e)
assert.Nil(t, err)
assert.Equal(t, gs.Status.State, string(agonesv1.GameServerStateAllocated))

_, err = l.Shutdown(ctx, e)
assert.Nil(t, err)

gs, err = l.GetGameServer(ctx, e)
assert.Nil(t, err)
assert.Equal(t, gs.Status.State, string(agonesv1.GameServerStateShutdown))

}
6 changes: 3 additions & 3 deletions test/sdk/go/sdk-client-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ func main() {
if err != nil {
log.Fatalf("Could not send ready message %s", err)
}
if err = sdk.Reserve(5 * time.Second); err != nil {
log.Fatalf("Could not send Reserve command: %s", err)
}
err = sdk.Allocate()
if err != nil {
log.Fatalf("Err sending allocate request %s", err)
Expand All @@ -90,9 +93,6 @@ func main() {
if err != nil {
log.Fatalf("Could not set annotation: %s", err)
}
if err = sdk.Reserve(5 * time.Second); err != nil {
log.Fatalf("Could not send Reserve command: %s", err)
}
err = sdk.Shutdown()
if err != nil {
log.Fatalf("Could not shutdown GameServer: %s", err)
Expand Down
9 changes: 5 additions & 4 deletions test/sdk/nodejs/testSDKClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ const connect = async () => {
}
});
await agonesSDK.ready();
setTimeout(() => {
console.log('send allocate request');
agonesSDK.allocate();
}, 1000);

let result = await agonesSDK.getGameServer();
await agonesSDK.setLabel('label', result.objectMeta.creationTimestamp.toString());
Expand All @@ -46,6 +42,11 @@ const connect = async () => {

await agonesSDK.reserve(5);

setTimeout(() => {
console.log('send allocate request');
agonesSDK.allocate();
}, 1000);

setTimeout(() => {
console.log('send shutdown request');
agonesSDK.shutdown();
Expand Down

0 comments on commit 74e0d3f

Please sign in to comment.