feat: game lobby service
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package redisstate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/ports"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// StreamLagProbe is the Redis-backed implementation of ports.StreamLagProbe.
|
||||
// It uses XRANGE with an exclusive start to find the oldest entry that
|
||||
// follows the saved consumer offset and parses the ms component of the
|
||||
// returned entry id.
|
||||
type StreamLagProbe struct {
|
||||
client *redis.Client
|
||||
clock func() time.Time
|
||||
}
|
||||
|
||||
// NewStreamLagProbe constructs one Redis-backed stream-lag probe. clock is
|
||||
// optional; when nil the probe falls back to time.Now.
|
||||
func NewStreamLagProbe(client *redis.Client, clock func() time.Time) (*StreamLagProbe, error) {
|
||||
if client == nil {
|
||||
return nil, errors.New("new lobby stream lag probe: nil redis client")
|
||||
}
|
||||
if clock == nil {
|
||||
clock = time.Now
|
||||
}
|
||||
return &StreamLagProbe{client: client, clock: clock}, nil
|
||||
}
|
||||
|
||||
// OldestUnprocessedAge returns the age of the first stream entry strictly
|
||||
// after savedOffset. When savedOffset is empty, the probe falls back to the
|
||||
// stream head. The boolean return reports whether an entry was found.
|
||||
func (probe *StreamLagProbe) OldestUnprocessedAge(ctx context.Context, stream, savedOffset string) (time.Duration, bool, error) {
|
||||
if probe == nil || probe.client == nil {
|
||||
return 0, false, errors.New("oldest unprocessed age: nil probe")
|
||||
}
|
||||
if ctx == nil {
|
||||
return 0, false, errors.New("oldest unprocessed age: nil context")
|
||||
}
|
||||
if strings.TrimSpace(stream) == "" {
|
||||
return 0, false, errors.New("oldest unprocessed age: empty stream name")
|
||||
}
|
||||
|
||||
start := "-"
|
||||
if trimmed := strings.TrimSpace(savedOffset); trimmed != "" {
|
||||
start = "(" + trimmed
|
||||
}
|
||||
|
||||
entries, err := probe.client.XRangeN(ctx, stream, start, "+", 1).Result()
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("oldest unprocessed age: %w", err)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
ms, err := parseStreamEntryMillis(entries[0].ID)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("oldest unprocessed age: %w", err)
|
||||
}
|
||||
|
||||
now := probe.clock()
|
||||
age := now.UnixMilli() - ms
|
||||
if age < 0 {
|
||||
return 0, true, nil
|
||||
}
|
||||
return time.Duration(age) * time.Millisecond, true, nil
|
||||
}
|
||||
|
||||
// parseStreamEntryMillis extracts the ms prefix from a Redis Stream entry
|
||||
// id of the form `<ms>-<seq>`. It returns an error when the format does
|
||||
// not match.
|
||||
func parseStreamEntryMillis(id string) (int64, error) {
|
||||
hyphen := strings.IndexByte(id, '-')
|
||||
if hyphen <= 0 {
|
||||
return 0, fmt.Errorf("malformed stream entry id %q", id)
|
||||
}
|
||||
ms, err := strconv.ParseInt(id[:hyphen], 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("malformed stream entry id %q: %w", id, err)
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
// Compile-time interface assertion.
|
||||
var _ ports.StreamLagProbe = (*StreamLagProbe)(nil)
|
||||
Reference in New Issue
Block a user