feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
@@ -0,0 +1,95 @@
package redisstate
import (
"context"
"errors"
"fmt"
"time"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/ports"
"github.com/redis/go-redis/v9"
)
// CapabilityEvaluationGuardTTL bounds how long the guard marker survives
// in Redis. The evaluator only reads the guard during `game_finished`
// processing, and capability windows expire after 30 days, so a 60-day
// retention is comfortably long enough to absorb any practical replay
// while still letting the keyspace reclaim space eventually.
const CapabilityEvaluationGuardTTL time.Duration = 60 * 24 * time.Hour
// EvaluationGuardStore stores per-game «already evaluated» markers in Redis
// using SETNX semantics. The first MarkEvaluated call for a gameID records
// the marker; later calls observe the existing key and return already=true.
type EvaluationGuardStore struct {
client *redis.Client
keys Keyspace
ttl time.Duration
}
// NewEvaluationGuardStore constructs one Redis-backed EvaluationGuardStore
// using the default guard TTL.
func NewEvaluationGuardStore(client *redis.Client) (*EvaluationGuardStore, error) {
if client == nil {
return nil, errors.New("new lobby evaluation guard store: nil redis client")
}
return &EvaluationGuardStore{
client: client,
keys: Keyspace{},
ttl: CapabilityEvaluationGuardTTL,
}, nil
}
// IsEvaluated reports whether gameID is already marked. It performs a
// single GET against the guard key and treats the missing-key case as
// not-yet-evaluated.
func (store *EvaluationGuardStore) IsEvaluated(ctx context.Context, gameID common.GameID) (bool, error) {
if store == nil || store.client == nil {
return false, errors.New("is evaluated: nil store")
}
if ctx == nil {
return false, errors.New("is evaluated: nil context")
}
if err := gameID.Validate(); err != nil {
return false, fmt.Errorf("is evaluated: %w", err)
}
_, err := store.client.Get(ctx, store.keys.CapabilityEvaluationGuard(gameID)).Result()
switch {
case err == nil:
return true, nil
case errors.Is(err, redis.Nil):
return false, nil
default:
return false, fmt.Errorf("is evaluated: %w", err)
}
}
// MarkEvaluated records gameID as evaluated. Calling MarkEvaluated twice
// for the same gameID is safe; the second call leaves the marker
// untouched and refreshes the TTL.
func (store *EvaluationGuardStore) MarkEvaluated(ctx context.Context, gameID common.GameID) error {
if store == nil || store.client == nil {
return errors.New("mark evaluated: nil store")
}
if ctx == nil {
return errors.New("mark evaluated: nil context")
}
if err := gameID.Validate(); err != nil {
return fmt.Errorf("mark evaluated: %w", err)
}
if err := store.client.Set(
ctx,
store.keys.CapabilityEvaluationGuard(gameID),
"1",
store.ttl,
).Err(); err != nil {
return fmt.Errorf("mark evaluated: %w", err)
}
return nil
}
// Compile-time interface assertion.
var _ ports.EvaluationGuardStore = (*EvaluationGuardStore)(nil)