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

feat: add occupied slots to game rooms api #641

Open
wants to merge 3 commits into
base: feature/multiple-matches
Choose a base branch
from
Open
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
19 changes: 18 additions & 1 deletion internal/adapters/storage/redis/room/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func (r redisStateStorage) GetRoom(ctx context.Context, scheduler, roomID string
roomHashCmd := r.client.HGetAll(ctx, getRoomRedisKey(room.SchedulerID, room.ID))
statusCmd := p.ZScore(ctx, getRoomStatusSetRedisKey(room.SchedulerID), room.ID)
pingCmd := p.ZScore(ctx, getRoomPingRedisKey(room.SchedulerID), room.ID)
occupancyCmd := p.ZScore(ctx, getRoomOccupancyRedisKey(room.SchedulerID), room.ID)
metrics.RunWithMetrics(roomStorageMetricLabel, func() error {
_, err = p.Exec(ctx)
return err
Expand All @@ -87,6 +88,7 @@ func (r redisStateStorage) GetRoom(ctx context.Context, scheduler, roomID string

room.Status = game_room.GameRoomStatus(statusCmd.Val())
room.LastPingAt = time.Unix(int64(pingCmd.Val()), 0)
room.OccupiedSlots = int(occupancyCmd.Val())
err = json.NewDecoder(strings.NewReader(roomHashCmd.Val()[metadataKey])).Decode(&room.Metadata)
if err != nil {
return nil, errors.NewErrEncoding("error unmarshalling room %s json", roomID).WithError(err)
Expand Down Expand Up @@ -126,6 +128,11 @@ func (r *redisStateStorage) CreateRoom(ctx context.Context, room *game_room.Game
createdAtKey: time.Now().Unix(),
})

occupancyCmd := p.ZAddNX(ctx, getRoomOccupancyRedisKey(room.SchedulerID), &redis.Z{
Member: room.ID,
Score: float64(room.OccupiedSlots),
})

statusCmd := p.ZAddNX(ctx, getRoomStatusSetRedisKey(room.SchedulerID), &redis.Z{
Member: room.ID,
Score: float64(room.Status),
Expand All @@ -144,7 +151,7 @@ func (r *redisStateStorage) CreateRoom(ctx context.Context, room *game_room.Game
return errors.NewErrUnexpected("error storing room %s on redis", room.ID).WithError(err)
}

if statusCmd.Val() < 1 || pingCmd.Val() < 1 {
if statusCmd.Val() < 1 || pingCmd.Val() < 1 || occupancyCmd.Val() < 1 {
return errors.NewErrAlreadyExists("room %s already exists in scheduler %s", room.ID, room.SchedulerID)
}

Expand Down Expand Up @@ -173,6 +180,11 @@ func (r *redisStateStorage) UpdateRoom(ctx context.Context, room *game_room.Game
Score: float64(room.LastPingAt.Unix()),
})

p.ZAddXXCh(ctx, getRoomOccupancyRedisKey(room.SchedulerID), &redis.Z{
Member: room.ID,
Score: float64(room.OccupiedSlots),
})

metrics.RunWithMetrics(roomStorageMetricLabel, func() error {
_, err = p.Exec(ctx)
return err
Expand All @@ -190,6 +202,7 @@ func (r *redisStateStorage) DeleteRoom(ctx context.Context, scheduler, roomID st
p.Del(ctx, getRoomRedisKey(scheduler, roomID))
p.ZRem(ctx, getRoomStatusSetRedisKey(scheduler), roomID)
p.ZRem(ctx, getRoomPingRedisKey(scheduler), roomID)
p.ZRem(ctx, getRoomOccupancyRedisKey(scheduler), roomID)
metrics.RunWithMetrics(roomStorageMetricLabel, func() error {
cmders, err = p.Exec(ctx)
return err
Expand Down Expand Up @@ -399,3 +412,7 @@ func getRoomPingRedisKey(scheduler string) string {
func getRoomStatusUpdateChannel(scheduler, roomID string) string {
return fmt.Sprintf("scheduler:%s:rooms:%s:updatechan", scheduler, roomID)
}

func getRoomOccupancyRedisKey(scheduler string) string {
return fmt.Sprintf("scheduler:%s:occupancy", scheduler)
}
70 changes: 49 additions & 21 deletions internal/adapters/storage/redis/room/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ func assertRedisState(t *testing.T, client *redis.Client, room *game_room.GameRo
pingCmd := client.ZScore(context.Background(), getRoomPingRedisKey(room.SchedulerID), room.ID)
require.NoError(t, pingCmd.Err())
require.Equal(t, float64(room.LastPingAt.Unix()), pingCmd.Val())

occupancyCmd := client.ZScore(context.Background(), getRoomOccupancyRedisKey(room.SchedulerID), room.ID)
require.NoError(t, occupancyCmd.Err())
require.Equal(t, float64(room.OccupiedSlots), occupancyCmd.Val())
}

func assertUpdateStatusEventPublished(t *testing.T, sub *redis.PubSub, room *game_room.GameRoom) {
Expand Down Expand Up @@ -114,6 +118,9 @@ func assertRedisStateNonExistent(t *testing.T, client *redis.Client, room *game_

pingCmd := client.ZScore(context.Background(), getRoomPingRedisKey(room.SchedulerID), room.ID)
require.Error(t, pingCmd.Err())

occupancyCmd := client.ZScore(context.Background(), getRoomOccupancyRedisKey(room.SchedulerID), room.ID)
require.Error(t, occupancyCmd.Err())
}

func requireErrorKind(t *testing.T, expected error, err error) {
Expand All @@ -134,6 +141,7 @@ func TestRedisStateStorage_CreateRoom(t *testing.T) {
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
IsValidationRoom: false,
OccupiedSlots: 10,
}
require.NoError(t, storage.CreateRoom(ctx, room))
assertRedisState(t, client, room)
Expand All @@ -150,6 +158,7 @@ func TestRedisStateStorage_CreateRoom(t *testing.T) {
Metadata: map[string]interface{}{
"region": "us",
},
OccupiedSlots: 10,
}

require.NoError(t, storage.CreateRoom(ctx, room))
Expand All @@ -167,6 +176,7 @@ func TestRedisStateStorage_CreateRoom(t *testing.T) {
Metadata: map[string]interface{}{
"region": "us",
},
OccupiedSlots: 10,
}

require.NoError(t, storage.CreateRoom(ctx, firstRoom))
Expand All @@ -182,6 +192,7 @@ func TestRedisStateStorage_CreateRoom(t *testing.T) {
Metadata: map[string]interface{}{
"region": "us",
},
OccupiedSlots: 10,
}

requireErrorKind(t, errors.ErrAlreadyExists, storage.CreateRoom(ctx, secondRoom))
Expand All @@ -202,6 +213,7 @@ func TestRedisStateStorage_UpdateRoom(t *testing.T) {
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
IsValidationRoom: false,
OccupiedSlots: 20,
}

require.NoError(t, storage.CreateRoom(ctx, room))
Expand All @@ -221,6 +233,7 @@ func TestRedisStateStorage_UpdateRoom(t *testing.T) {
Metadata: map[string]interface{}{
"region": "us",
},
OccupiedSlots: 20,
}

require.NoError(t, storage.CreateRoom(ctx, room))
Expand All @@ -237,11 +250,12 @@ func TestRedisStateStorage_DeleteRoom(t *testing.T) {

t.Run("game room exists", func(t *testing.T) {
room := &game_room.GameRoom{
ID: "room-1",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
ID: "room-1",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
OccupiedSlots: 20,
}

require.NoError(t, storage.CreateRoom(ctx, room))
Expand All @@ -262,12 +276,13 @@ func TestRedisStateStorage_GetRoom(t *testing.T) {
t.Run("game room without metadata", func(t *testing.T) {
createdAt := time.Unix(time.Date(2020, 1, 1, 2, 2, 0, 0, time.UTC).Unix(), 0)
expectedRoom := &game_room.GameRoom{
ID: "room-1",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
CreatedAt: createdAt,
ID: "room-1",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
CreatedAt: createdAt,
OccupiedSlots: 30,
}
metadataJson, _ := json.Marshal(expectedRoom.Metadata)

Expand All @@ -290,6 +305,11 @@ func TestRedisStateStorage_GetRoom(t *testing.T) {
Score: float64(expectedRoom.LastPingAt.Unix()),
})

_ = p.ZAddNX(ctx, getRoomOccupancyRedisKey(expectedRoom.SchedulerID), &redis.Z{
Member: expectedRoom.ID,
Score: float64(expectedRoom.OccupiedSlots),
})

_, _ = p.Exec(ctx)

actualRoom, err := storage.GetRoom(ctx, expectedRoom.SchedulerID, expectedRoom.ID)
Expand All @@ -310,6 +330,7 @@ func TestRedisStateStorage_GetRoom(t *testing.T) {
},
IsValidationRoom: true,
CreatedAt: createdAt,
OccupiedSlots: 30,
}

metadataJson, _ := json.Marshal(expectedRoom.Metadata)
Expand All @@ -332,6 +353,11 @@ func TestRedisStateStorage_GetRoom(t *testing.T) {
Member: expectedRoom.ID,
Score: float64(expectedRoom.LastPingAt.Unix()),
})

_ = p.ZAddNX(ctx, getRoomOccupancyRedisKey(expectedRoom.SchedulerID), &redis.Z{
Member: expectedRoom.ID,
Score: float64(expectedRoom.OccupiedSlots),
})
_, _ = p.Exec(ctx)

actualRoom, err := storage.GetRoom(ctx, expectedRoom.SchedulerID, expectedRoom.ID)
Expand Down Expand Up @@ -605,11 +631,12 @@ func TestRedisStateStorage_UpdateRoomStatus(t *testing.T) {

t.Run("game room updates room status", func(t *testing.T) {
room := &game_room.GameRoom{
ID: "room-1",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
ID: "room-1",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
OccupiedSlots: 40,
}

sub := client.Subscribe(context.Background(), getRoomStatusUpdateChannel(room.SchedulerID, room.ID))
Expand All @@ -624,11 +651,12 @@ func TestRedisStateStorage_UpdateRoomStatus(t *testing.T) {

t.Run("game room doesn't update room status when room doesn't exists", func(t *testing.T) {
room := &game_room.GameRoom{
ID: "room-not-found",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
ID: "room-not-found",
SchedulerID: "game",
Version: "1.0",
Status: game_room.GameStatusReady,
LastPingAt: lastPing,
OccupiedSlots: 40,
}

sub := client.Subscribe(context.Background(), getRoomStatusUpdateChannel(room.SchedulerID, room.ID))
Expand Down
11 changes: 6 additions & 5 deletions internal/api/handlers/requestadapters/rooms.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ func FromApiUpdateRoomRequestToEntity(request *api.UpdateRoomWithPingRequest) (*
}

return &game_room.GameRoom{
ID: request.GetRoomName(),
SchedulerID: request.GetSchedulerName(),
PingStatus: status,
Metadata: request.Metadata.AsMap(),
LastPingAt: time.Unix(request.GetTimestamp(), 0),
ID: request.GetRoomName(),
SchedulerID: request.GetSchedulerName(),
PingStatus: status,
Metadata: request.Metadata.AsMap(),
LastPingAt: time.Unix(request.GetTimestamp(), 0),
OccupiedSlots: int(request.GetRunningMatches()),
}, nil
}

Expand Down
24 changes: 14 additions & 10 deletions internal/api/handlers/requestadapters/rooms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestFromApiUpdateRoomRequestToEntity(t *testing.T) {

genericString := "some-value"
genericTime := time.Now()
occupiedSlots := 10

testCases := []struct {
Title string
Expand All @@ -58,11 +59,12 @@ func TestFromApiUpdateRoomRequestToEntity(t *testing.T) {
Title: "receives valid ping request, returns gameRoom entity",
Input: Input{
PingRequest: &api.UpdateRoomWithPingRequest{
SchedulerName: genericString,
RoomName: genericString,
Metadata: nil,
Status: "ready",
Timestamp: int64(genericTime.Second()),
SchedulerName: genericString,
RoomName: genericString,
Metadata: nil,
Status: "ready",
Timestamp: int64(genericTime.Second()),
RunningMatches: int64(occupiedSlots),
},
},
Output: Output{
Expand All @@ -75,18 +77,20 @@ func TestFromApiUpdateRoomRequestToEntity(t *testing.T) {
Metadata: map[string]interface{}{},
LastPingAt: time.Unix(int64(genericTime.Second()), 0),
IsValidationRoom: false,
OccupiedSlots: occupiedSlots,
},
},
},
{
Title: "receives invalid ping request, returns err and gameRoom nil",
Input: Input{
PingRequest: &api.UpdateRoomWithPingRequest{
SchedulerName: genericString,
RoomName: genericString,
Metadata: nil,
Status: "INVALID_PING",
Timestamp: int64(genericTime.Second()),
SchedulerName: genericString,
RoomName: genericString,
Metadata: nil,
Status: "INVALID_PING",
Timestamp: int64(genericTime.Second()),
RunningMatches: int64(occupiedSlots),
},
},
Output: Output{
Expand Down
5 changes: 3 additions & 2 deletions internal/api/handlers/rooms_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,9 @@ func TestRoomsHandler_UpdateRoomStatus(t *testing.T) {
},
},
},
Status: "ready",
Timestamp: 0,
Status: "ready",
Timestamp: 0,
RunningMatches: 20,
},
},
&api.UpdateRoomStatusResponse{Success: true},
Expand Down
1 change: 1 addition & 0 deletions internal/core/entities/game_room/game_room.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ type GameRoom struct {
IsValidationRoom bool
LastPingAt time.Time
CreatedAt time.Time
OccupiedSlots int
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: why don't we use the same naming as the proto? IMO it would simplify maintenance and they seem to map 1:1, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: why don't we use the same naming as the proto? IMO it would simplify maintenance and they seem to map 1:1, right?

}

// validStatusTransitions this map has all possible status changes for a game
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/v1/messages.pb.go

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

2 changes: 1 addition & 1 deletion pkg/api/v1/operations.pb.go

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

Loading
Loading