// 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)