feat: game lobby service
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// Package metricsracenamedir wraps a ports.RaceNameDirectory with the
|
||||
// `lobby.race_name.outcomes` counter from `lobby/README.md` §Observability.
|
||||
package metricsracenamedir
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/ports"
|
||||
"galaxy/lobby/internal/telemetry"
|
||||
)
|
||||
|
||||
// Directory decorates an inner ports.RaceNameDirectory and emits a
|
||||
// `lobby.race_name.outcomes` increment per successful side-effect call.
|
||||
//
|
||||
// Errors do not increment the counter — the README outcome vocabulary only
|
||||
// enumerates positive outcomes.
|
||||
type Directory struct {
|
||||
inner ports.RaceNameDirectory
|
||||
telemetry *telemetry.Runtime
|
||||
}
|
||||
|
||||
// New constructs one Directory around inner. When telemetryRuntime is nil,
|
||||
// the wrapper still delegates each call but does not record metrics.
|
||||
func New(inner ports.RaceNameDirectory, telemetryRuntime *telemetry.Runtime) *Directory {
|
||||
return &Directory{inner: inner, telemetry: telemetryRuntime}
|
||||
}
|
||||
|
||||
// Canonicalize forwards to the inner directory; no metric is recorded.
|
||||
func (directory *Directory) Canonicalize(raceName string) (string, error) {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return "", nil
|
||||
}
|
||||
return directory.inner.Canonicalize(raceName)
|
||||
}
|
||||
|
||||
// Check forwards to the inner directory; no metric is recorded.
|
||||
func (directory *Directory) Check(ctx context.Context, raceName, actorUserID string) (ports.Availability, error) {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return ports.Availability{}, nil
|
||||
}
|
||||
return directory.inner.Check(ctx, raceName, actorUserID)
|
||||
}
|
||||
|
||||
// Reserve emits `outcome=reserved` after a successful inner call.
|
||||
func (directory *Directory) Reserve(ctx context.Context, gameID, userID, raceName string) error {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil
|
||||
}
|
||||
if err := directory.inner.Reserve(ctx, gameID, userID, raceName); err != nil {
|
||||
return err
|
||||
}
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "reserved")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseReservation emits `outcome=reservation_released` after a
|
||||
// successful inner call. Per the inner contract a successful return covers
|
||||
// both real releases and harmless no-ops; the metric counts release
|
||||
// attempts that completed without error.
|
||||
func (directory *Directory) ReleaseReservation(ctx context.Context, gameID, userID, raceName string) error {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil
|
||||
}
|
||||
if err := directory.inner.ReleaseReservation(ctx, gameID, userID, raceName); err != nil {
|
||||
return err
|
||||
}
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "reservation_released")
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkPendingRegistration emits `outcome=pending_created` after a
|
||||
// successful inner call.
|
||||
func (directory *Directory) MarkPendingRegistration(
|
||||
ctx context.Context,
|
||||
gameID, userID, raceName string,
|
||||
eligibleUntil time.Time,
|
||||
) error {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil
|
||||
}
|
||||
if err := directory.inner.MarkPendingRegistration(ctx, gameID, userID, raceName, eligibleUntil); err != nil {
|
||||
return err
|
||||
}
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "pending_created")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpirePendingRegistrations emits `outcome=pending_released` once per
|
||||
// returned expired entry.
|
||||
func (directory *Directory) ExpirePendingRegistrations(ctx context.Context, now time.Time) ([]ports.ExpiredPending, error) {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil, nil
|
||||
}
|
||||
expired, err := directory.inner.ExpirePendingRegistrations(ctx, now)
|
||||
if err != nil {
|
||||
return expired, err
|
||||
}
|
||||
for range expired {
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "pending_released")
|
||||
}
|
||||
return expired, nil
|
||||
}
|
||||
|
||||
// Register emits `outcome=registered` after a successful inner call.
|
||||
func (directory *Directory) Register(ctx context.Context, gameID, userID, raceName string) error {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil
|
||||
}
|
||||
if err := directory.inner.Register(ctx, gameID, userID, raceName); err != nil {
|
||||
return err
|
||||
}
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "registered")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRegistered forwards to the inner directory; no metric is recorded.
|
||||
func (directory *Directory) ListRegistered(ctx context.Context, userID string) ([]ports.RegisteredName, error) {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return directory.inner.ListRegistered(ctx, userID)
|
||||
}
|
||||
|
||||
// ListPendingRegistrations forwards to the inner directory; no metric is
|
||||
// recorded.
|
||||
func (directory *Directory) ListPendingRegistrations(ctx context.Context, userID string) ([]ports.PendingRegistration, error) {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return directory.inner.ListPendingRegistrations(ctx, userID)
|
||||
}
|
||||
|
||||
// ListReservations forwards to the inner directory; no metric is recorded.
|
||||
func (directory *Directory) ListReservations(ctx context.Context, userID string) ([]ports.Reservation, error) {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return directory.inner.ListReservations(ctx, userID)
|
||||
}
|
||||
|
||||
// ReleaseAllByUser snapshots the per-kind counts via List* before invoking
|
||||
// the inner cascade, then emits one
|
||||
// `reservation_released`/`pending_released`/`registered_released` per
|
||||
// snapshotted entry on success. The pre-call snapshot is non-atomic
|
||||
// relative to the cascade itself; telemetry counts are advisory and
|
||||
// tolerate this race.
|
||||
func (directory *Directory) ReleaseAllByUser(ctx context.Context, userID string) error {
|
||||
if directory == nil || directory.inner == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
reservations, _ := directory.inner.ListReservations(ctx, userID)
|
||||
pending, _ := directory.inner.ListPendingRegistrations(ctx, userID)
|
||||
registered, _ := directory.inner.ListRegistered(ctx, userID)
|
||||
|
||||
if err := directory.inner.ReleaseAllByUser(ctx, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for range reservations {
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "reservation_released")
|
||||
}
|
||||
for range pending {
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "pending_released")
|
||||
}
|
||||
for range registered {
|
||||
directory.telemetry.RecordRaceNameOutcome(ctx, "registered_released")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile-time interface assertion.
|
||||
var _ ports.RaceNameDirectory = (*Directory)(nil)
|
||||
Reference in New Issue
Block a user