96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
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)
|