feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+12 -22
View File
@@ -22,7 +22,7 @@ const (
initialEntitlementActorType common.ActorType = "service"
initialEntitlementActorID common.ActorID = "user-service"
ensureCreateRetryLimit = 8
ensureCreateRetryLimit = 10
)
// ResolveByEmailInput stores one auth-facing resolve-by-email request.
@@ -155,7 +155,6 @@ type Ensurer struct {
store ports.AuthDirectoryStore
clock ports.Clock
idGenerator ports.IDGenerator
policy ports.RaceNamePolicy
logger *slog.Logger
telemetry *telemetry.Runtime
profilePublisher ports.ProfileChangedPublisher
@@ -168,9 +167,8 @@ func NewEnsurer(
store ports.AuthDirectoryStore,
clock ports.Clock,
idGenerator ports.IDGenerator,
policy ports.RaceNamePolicy,
) (*Ensurer, error) {
return NewEnsurerWithObservability(store, clock, idGenerator, policy, nil, nil, nil, nil, nil)
return NewEnsurerWithObservability(store, clock, idGenerator, nil, nil, nil, nil, nil)
}
// NewEnsurerWithObservability returns one ensure-by-email use case instance
@@ -180,7 +178,6 @@ func NewEnsurerWithObservability(
store ports.AuthDirectoryStore,
clock ports.Clock,
idGenerator ports.IDGenerator,
policy ports.RaceNamePolicy,
logger *slog.Logger,
telemetryRuntime *telemetry.Runtime,
profilePublisher ports.ProfileChangedPublisher,
@@ -194,14 +191,11 @@ func NewEnsurerWithObservability(
return nil, fmt.Errorf("authdirectory ensurer: clock must not be nil")
case idGenerator == nil:
return nil, fmt.Errorf("authdirectory ensurer: id generator must not be nil")
case policy == nil:
return nil, fmt.Errorf("authdirectory ensurer: race-name policy must not be nil")
default:
return &Ensurer{
store: store,
clock: clock,
idGenerator: idGenerator,
policy: policy,
logger: logger,
telemetry: telemetryRuntime,
profilePublisher: profilePublisher,
@@ -256,7 +250,7 @@ func (service *Ensurer) Execute(ctx context.Context, input EnsureByEmailInput) (
if err != nil {
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
}
raceName, err := service.idGenerator.NewInitialRaceName()
userName, err := service.idGenerator.NewUserName()
if err != nil {
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
}
@@ -264,7 +258,7 @@ func (service *Ensurer) Execute(ctx context.Context, input EnsureByEmailInput) (
accountRecord := account.UserAccount{
UserID: userID,
Email: email,
RaceName: raceName,
UserName: userName,
PreferredLanguage: preferredLanguage,
TimeZone: timeZone,
CreatedAt: now,
@@ -294,21 +288,16 @@ func (service *Ensurer) Execute(ctx context.Context, input EnsureByEmailInput) (
StartsAt: now,
CreatedAt: now,
}
reservation, err := shared.BuildRaceNameReservation(service.policy, userID, raceName, now)
if err != nil {
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
}
ensureResult, err := service.store.EnsureByEmail(ctx, ports.EnsureByEmailInput{
Email: email,
Account: accountRecord,
Entitlement: entitlementSnapshot,
EntitlementRecord: entitlementRecord,
Reservation: reservation,
})
if err != nil {
if errors.Is(err, ports.ErrRaceNameConflict) && service.telemetry != nil {
service.telemetry.RecordRaceNameReservationConflict(ctx, "ensure_by_email")
if errors.Is(err, ports.ErrUserNameConflict) && service.telemetry != nil {
service.telemetry.RecordUserNameConflict(ctx, "ensure_by_email")
}
if errors.Is(err, ports.ErrConflict) {
continue
@@ -349,11 +338,12 @@ func (service *Ensurer) publishInitializedEvents(
occurredAt := accountRecord.UpdatedAt.UTC()
service.publishProfileChanged(ctx, ports.ProfileChangedEvent{
UserID: accountRecord.UserID,
OccurredAt: occurredAt,
Source: initialEntitlementSource,
Operation: ports.ProfileChangedOperationInitialized,
RaceName: accountRecord.RaceName,
UserID: accountRecord.UserID,
OccurredAt: occurredAt,
Source: initialEntitlementSource,
Operation: ports.ProfileChangedOperationInitialized,
UserName: accountRecord.UserName,
DisplayName: accountRecord.DisplayName,
})
service.publishSettingsChanged(ctx, ports.SettingsChangedEvent{
UserID: accountRecord.UserID,
@@ -6,7 +6,6 @@ import (
"testing"
"time"
"galaxy/user/internal/domain/account"
"galaxy/user/internal/domain/common"
"galaxy/user/internal/domain/entitlement"
"galaxy/user/internal/domain/policy"
@@ -101,12 +100,9 @@ func TestEnsurerExecuteCreatedBuildsInitialRecords(t *testing.T) {
ensureByEmail: func(_ context.Context, input ports.EnsureByEmailInput) (ports.EnsureByEmailResult, error) {
require.Equal(t, common.Email("created@example.com"), input.Email)
require.Equal(t, common.UserID("user-created"), input.Account.UserID)
require.Equal(t, common.RaceName("player-test123"), input.Account.RaceName)
require.Equal(t, common.UserName("player-test123"), input.Account.UserName)
require.Equal(t, common.LanguageTag("en-US"), input.Account.PreferredLanguage)
require.Equal(t, common.TimeZoneName("Europe/Kaliningrad"), input.Account.TimeZone)
require.Equal(t, input.Account.UserID, input.Reservation.UserID)
require.Equal(t, input.Account.RaceName, input.Reservation.RaceName)
require.Equal(t, accountTestCanonicalKey(input.Account.RaceName), input.Reservation.CanonicalKey)
require.Equal(t, entitlement.PlanCodeFree, input.Entitlement.PlanCode)
require.False(t, input.Entitlement.IsPaid)
require.Equal(t, input.Account.UserID, input.Entitlement.UserID)
@@ -124,9 +120,9 @@ func TestEnsurerExecuteCreatedBuildsInitialRecords(t *testing.T) {
},
}, fixedClock{now: now}, fixedIDGenerator{
userID: common.UserID("user-created"),
raceName: common.RaceName("player-test123"),
userName: common.UserName("player-test123"),
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
}, stubRaceNamePolicy{})
})
require.NoError(t, err)
result, err := ensurer.Execute(context.Background(), EnsureByEmailInput{
@@ -180,9 +176,9 @@ func TestEnsurerExecuteRejectsInvalidRegistrationContext(t *testing.T) {
ensurer, err := NewEnsurer(stubAuthDirectoryStore{}, fixedClock{now: time.Unix(1_775_240_000, 0).UTC()}, fixedIDGenerator{
userID: common.UserID("user-created"),
raceName: common.RaceName("player-test123"),
userName: common.UserName("player-test123"),
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
}, stubRaceNamePolicy{})
})
require.NoError(t, err)
_, err = ensurer.Execute(context.Background(), tt.input)
@@ -210,9 +206,9 @@ func TestEnsurerExecuteRetriesConflicts(t *testing.T) {
},
}, fixedClock{now: time.Unix(1_775_240_000, 0).UTC()}, &sequenceIDGenerator{
userIDs: []common.UserID{"user-first", "user-second"},
raceNames: []common.RaceName{"player-first", "player-second"},
userNames: []common.UserName{"player-firstxyz", "player-secondxy"},
entitlementRecordIDs: []entitlement.EntitlementRecordID{"entitlement-first", "entitlement-second"},
}, stubRaceNamePolicy{})
})
require.NoError(t, err)
result, err := ensurer.Execute(context.Background(), EnsureByEmailInput{
@@ -276,9 +272,9 @@ func TestEnsurerExecuteReturnsExistingAndBlocked(t *testing.T) {
ensurer, err := NewEnsurer(tt.store, fixedClock{now: time.Unix(1_775_240_000, 0).UTC()}, fixedIDGenerator{
userID: common.UserID("user-created"),
raceName: common.RaceName("player-test123"),
userName: common.UserName("player-test123"),
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
}, stubRaceNamePolicy{})
})
require.NoError(t, err)
result, err := ensurer.Execute(context.Background(), EnsureByEmailInput{
@@ -310,9 +306,9 @@ func TestEnsurerExecuteCreatedPublishesInitializedEvents(t *testing.T) {
},
}, fixedClock{now: now}, fixedIDGenerator{
userID: common.UserID("user-created"),
raceName: common.RaceName("player-test123"),
userName: common.UserName("player-test123"),
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
}, stubRaceNamePolicy{}, nil, telemetryRuntime, publisher, publisher, publisher)
}, nil, telemetryRuntime, publisher, publisher, publisher)
require.NoError(t, err)
result, err := ensurer.Execute(context.Background(), EnsureByEmailInput{
@@ -407,9 +403,9 @@ func TestEnsurerExecuteExistingBlockedAndFailedDoNotPublishEvents(t *testing.T)
telemetryRuntime, reader := newObservedAuthTelemetryRuntime(t)
ensurer, err := NewEnsurerWithObservability(tt.store, fixedClock{now: time.Unix(1_775_240_000, 0).UTC()}, fixedIDGenerator{
userID: common.UserID("user-created"),
raceName: common.RaceName("player-test123"),
userName: common.UserName("player-test123"),
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
}, stubRaceNamePolicy{}, nil, telemetryRuntime, publisher, publisher, publisher)
}, nil, telemetryRuntime, publisher, publisher, publisher)
require.NoError(t, err)
_, err = ensurer.Execute(context.Background(), tt.input)
@@ -446,9 +442,9 @@ func TestEnsurerExecutePublishFailureDoesNotRollbackCreatedUser(t *testing.T) {
},
}, fixedClock{now: now}, fixedIDGenerator{
userID: common.UserID("user-created"),
raceName: common.RaceName("player-test123"),
userName: common.UserName("player-test123"),
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
}, stubRaceNamePolicy{}, nil, telemetryRuntime, publisher, publisher, publisher)
}, nil, telemetryRuntime, publisher, publisher, publisher)
require.NoError(t, err)
result, err := ensurer.Execute(context.Background(), EnsureByEmailInput{
@@ -546,7 +542,7 @@ func (clock fixedClock) Now() time.Time {
type fixedIDGenerator struct {
userID common.UserID
raceName common.RaceName
userName common.UserName
entitlementRecordID entitlement.EntitlementRecordID
sanctionRecordID policy.SanctionRecordID
limitRecordID policy.LimitRecordID
@@ -556,8 +552,8 @@ func (generator fixedIDGenerator) NewUserID() (common.UserID, error) {
return generator.userID, nil
}
func (generator fixedIDGenerator) NewInitialRaceName() (common.RaceName, error) {
return generator.raceName, nil
func (generator fixedIDGenerator) NewUserName() (common.UserName, error) {
return generator.userName, nil
}
func (generator fixedIDGenerator) NewEntitlementRecordID() (entitlement.EntitlementRecordID, error) {
@@ -574,7 +570,7 @@ func (generator fixedIDGenerator) NewLimitRecordID() (policy.LimitRecordID, erro
type sequenceIDGenerator struct {
userIDs []common.UserID
raceNames []common.RaceName
userNames []common.UserName
entitlementRecordIDs []entitlement.EntitlementRecordID
sanctionRecordIDs []policy.SanctionRecordID
limitRecordIDs []policy.LimitRecordID
@@ -586,9 +582,9 @@ func (generator *sequenceIDGenerator) NewUserID() (common.UserID, error) {
return value, nil
}
func (generator *sequenceIDGenerator) NewInitialRaceName() (common.RaceName, error) {
value := generator.raceNames[0]
generator.raceNames = generator.raceNames[1:]
func (generator *sequenceIDGenerator) NewUserName() (common.UserName, error) {
value := generator.userNames[0]
generator.userNames = generator.userNames[1:]
return value, nil
}
@@ -610,16 +606,6 @@ func (generator *sequenceIDGenerator) NewLimitRecordID() (policy.LimitRecordID,
return value, nil
}
type stubRaceNamePolicy struct{}
func (stubRaceNamePolicy) CanonicalKey(raceName common.RaceName) (account.RaceNameCanonicalKey, error) {
return accountTestCanonicalKey(raceName), nil
}
func accountTestCanonicalKey(raceName common.RaceName) account.RaceNameCanonicalKey {
return account.RaceNameCanonicalKey("key:" + raceName.String())
}
type recordingAuthDomainEventPublisher struct {
err error
profileEvents []ports.ProfileChangedEvent
@@ -710,7 +696,6 @@ var (
_ ports.Clock = fixedClock{}
_ ports.IDGenerator = fixedIDGenerator{}
_ ports.IDGenerator = (*sequenceIDGenerator)(nil)
_ ports.RaceNamePolicy = stubRaceNamePolicy{}
_ ports.ProfileChangedPublisher = (*recordingAuthDomainEventPublisher)(nil)
_ ports.SettingsChangedPublisher = (*recordingAuthDomainEventPublisher)(nil)
_ ports.EntitlementChangedPublisher = (*recordingAuthDomainEventPublisher)(nil)