feat: game lobby service
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user