diff --git a/internal/signaling/handler.go b/internal/signaling/handler.go index 1ce2346..c460f85 100644 --- a/internal/signaling/handler.go +++ b/internal/signaling/handler.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "net/http" - "strings" "sync" "time" @@ -19,12 +18,32 @@ import ( const MaxConnectionTime = 1 * time.Hour +const LobbyCleanInterval = 30 * time.Minute +const LobbyCleanThreshold = 24 * time.Hour + func Handler(ctx context.Context, store stores.Store, cloudflare *cloudflare.CredentialsClient) (*sync.WaitGroup, http.HandlerFunc) { manager := &TimeoutManager{ Store: store, } go manager.Run(ctx) + go func() { + logger := logging.GetLogger(ctx) + ticker := time.NewTicker(LobbyCleanInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + logger.Info("cleaning empty lobbies") + if err := store.CleanEmptyLobbies(ctx, time.Now().Add(-LobbyCleanThreshold)); err != nil { + logger.Error("failed to clean empty lobbies", zap.Error(err)) + } + case <-ctx.Done(): + return + } + } + }() + wg := &sync.WaitGroup{} return wg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -34,17 +53,10 @@ func Handler(ctx context.Context, store stores.Store, cloudflare *cloudflare.Cre ctx, cancel := context.WithTimeout(ctx, MaxConnectionTime) defer cancel() - userAgentLower := strings.ToLower(r.Header.Get("User-Agent")) - isSafari := strings.Contains(userAgentLower, "safari") && !strings.Contains(userAgentLower, "chrome") && !strings.Contains(userAgentLower, "android") acceptOptions := &websocket.AcceptOptions{ - // Allow any origin/game to connect. - InsecureSkipVerify: true, - } - - if isSafari { - acceptOptions.CompressionMode = websocket.CompressionDisabled + InsecureSkipVerify: true, // Allow any origin/game to connect. + CompressionMode: websocket.CompressionDisabled, } - conn, err := websocket.Accept(w, r, acceptOptions) if err != nil { util.ErrorAndAbort(w, r, http.StatusBadRequest, "", err) diff --git a/internal/signaling/stores/postgres.go b/internal/signaling/stores/postgres.go index d80c483..46e898f 100644 --- a/internal/signaling/stores/postgres.go +++ b/internal/signaling/stores/postgres.go @@ -151,11 +151,12 @@ func (s *PostgresStore) CreateLobby(ctx context.Context, game, lobbyCode, peerID logger.Warn("peer id too long", zap.String("peerID", peerID)) return ErrInvalidPeerID } + now := util.Now(ctx) res, err := s.DB.Exec(ctx, ` - INSERT INTO lobbies (code, game, public) - VALUES ($1, $2, true) + INSERT INTO lobbies (code, game, public, created_at, updated_at) + VALUES ($1, $2, true, $3, $3) ON CONFLICT DO NOTHING - `, lobbyCode, game) + `, lobbyCode, game, now) if err != nil { return err } @@ -171,6 +172,9 @@ func (s *PostgresStore) JoinLobby(ctx context.Context, game, lobbyCode, peerID s logger.Warn("peer id too long", zap.String("peerID", peerID)) return nil, ErrInvalidPeerID } + + now := util.Now(ctx) + tx, err := s.DB.Begin(ctx) if err != nil { return nil, err @@ -200,10 +204,12 @@ func (s *PostgresStore) JoinLobby(ctx context.Context, game, lobbyCode, peerID s _, err = tx.Exec(ctx, ` UPDATE lobbies - SET peers = array_append(peers, $1) - WHERE code = $2 - AND game = $3 - `, peerID, lobbyCode, game) + SET + peers = array_append(peers, $1), + updated_at = $2 + WHERE code = $3 + AND game = $4 + `, peerID, now, lobbyCode, game) if err != nil { return nil, err } @@ -232,14 +238,18 @@ func (s *PostgresStore) IsPeerInLobby(ctx context.Context, game, lobbyCode, peer } func (s *PostgresStore) LeaveLobby(ctx context.Context, game, lobbyCode, peerID string) ([]string, error) { + now := util.Now(ctx) + var peerlist []string err := s.DB.QueryRow(ctx, ` UPDATE lobbies - SET peers = array_remove(peers, $1) - WHERE code = $2 - AND game = $3 + SET + peers = array_remove(peers, $1), + updated_at = $2 + WHERE code = $3 + AND game = $4 RETURNING peers - `, peerID, lobbyCode, game).Scan(&peerlist) + `, peerID, now, lobbyCode, game).Scan(&peerlist) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return nil, err } @@ -392,3 +402,12 @@ func (s *PostgresStore) ClaimNextTimedOutPeer(ctx context.Context, threshold tim return true, tx.Commit(ctx) } + +func (s *PostgresStore) CleanEmptyLobbies(ctx context.Context, olderThan time.Time) error { + _, err := s.DB.Exec(ctx, ` + DELETE FROM lobbies + WHERE updated_at < $1 + AND peers = '{}' + `, olderThan) + return err +} diff --git a/internal/signaling/stores/shared.go b/internal/signaling/stores/shared.go index 397e95e..f02442f 100644 --- a/internal/signaling/stores/shared.go +++ b/internal/signaling/stores/shared.go @@ -29,6 +29,8 @@ type Store interface { TimeoutPeer(ctx context.Context, peerID, secret, gameID string, lobbies []string) error ReconnectPeer(ctx context.Context, peerID, secret, gameID string) (bool, []string, error) ClaimNextTimedOutPeer(ctx context.Context, threshold time.Duration, callback func(peerID, gameID string, lobbies []string) error) (bool, error) + + CleanEmptyLobbies(ctx context.Context, olderThan time.Time) error } type Lobby struct {