package service import ( "encoding/json" "fmt" "reflect" "sort" "strings" "time" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) const defaultGameClientBridgeLeaseDuration = 60 * time.Second const defaultGameClientBridgeCommandRetention = 30 * 24 * time.Hour type gameClientBridgeComponentSession struct { Session domain.ClientManagerSession Installation domain.ClientManagerInstallation } func (svc *CoreService) QueueGameClientBridgeCommandForSession(sessionID string, request domain.GameClientBridgeQueueRequest) (domain.GameClientBridgeCommand, error) { request.Payload = domain.CopyGameClientBridgePayload(request.Payload) if err := validator.ValidateGameClientBridgeQueueRequest(request); err != nil { return domain.GameClientBridgeCommand{}, err } user, err := svc.GetCurrentUser(sessionID) if err != nil { return domain.GameClientBridgeCommand{}, err } instance, err := svc.store.ServerInstances().Get(request.ServerInstanceID) if err != nil { return domain.GameClientBridgeCommand{}, err } if !canAccessServer(user, instance) { return domain.GameClientBridgeCommand{}, ErrForbidden } if instance.PluginID != request.PluginID { return domain.GameClientBridgeCommand{}, validationError("bridge command plugin must match server instance") } return svc.queueGameClientBridgeCommand(user.ID, request) } func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverInstanceID string) (domain.GameClientBridgeStatus, error) { if err := svc.authorizeServerLifecycle(sessionID, serverInstanceID); err != nil { return domain.GameClientBridgeStatus{}, err } instance, err := svc.store.ServerInstances().Get(serverInstanceID) if err != nil { return domain.GameClientBridgeStatus{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.GameClientBridgeStatus{}, err } status := domain.GameClientBridgeStatus{ServerInstanceID: instance.ID, PluginID: plugin.ID, Reason: "plugin does not declare a game client bridge profile", Profiles: []domain.GameClientBridgeProfileDeclaration{}, Features: []domain.GameClientBridgeFeatureAvailability{}} installations, err := svc.store.ClientManagerInstallations().List(domain.ClientManagerInstallationFilter{ServerInstanceID: instance.ID}) if err != nil { return domain.GameClientBridgeStatus{}, err } for _, profile := range plugin.RuntimeProfiles.ClientManagers { if !containsString(profile.Health.RequiredCapabilities, gameClientBridgeCapability) { continue } declaration := domain.GameClientBridgeProfileDeclaration{PluginID: plugin.ID, ProfileKey: profile.Key, Reason: "compatible companion session is offline", CommandTypes: gameClientBridgeCommandTypes(plugin.GameClientBridge.Commands), SnapshotTypes: gameClientBridgeSnapshotTypes(plugin.GameClientBridge.Snapshots), QueryTemplateKeys: gameClientBridgeQueryTemplateKeys(plugin.GameClientBridge.QueryTemplates)} for _, installation := range installations { if installation.ProfileKey != profile.Key || (installation.Status != domain.ClientManagerLifecycleOnline && installation.Status != domain.ClientManagerLifecycleDegraded) || installation.RequiresRedeploy { continue } sessions, listErr := svc.store.ClientManagerSessions().List(domain.ClientManagerSessionFilter{InstallationID: installation.ID, Status: domain.ClientManagerSessionActive}) if listErr != nil { return domain.GameClientBridgeStatus{}, listErr } for _, session := range sessions { if svc.now().Before(session.ExpiresAt) && containsString(session.Capabilities, gameClientBridgeCapability) && session.KeyGeneration == installation.KeyGeneration && session.DeploymentGeneration == installation.DeploymentGeneration && session.ArtifactID == installation.ActiveArtifactID { declaration.Available = true declaration.Reason = "" declaration.HandlerTypes = append(declaration.HandlerTypes, gameClientBridgeSessionCapabilityValues(session.Capabilities, "handler.")...) declaration.EventProducerTypes = append(declaration.EventProducerTypes, gameClientBridgeSessionCapabilityValues(session.Capabilities, "event-producer.")...) break } } } status.Profiles = append(status.Profiles, declaration) status.Available = status.Available || declaration.Available } if len(status.Profiles) > 0 { status.Reason = "no compatible companion session is online" } if status.Available { status.Reason = "" } status.Features = gameClientBridgeFeatureAvailability(plugin.GameClientBridge.Features, status.Profiles) return domain.CopyGameClientBridgeStatus(status), nil } func gameClientBridgeSessionCapabilityValues(capabilities []string, prefix string) []string { values := make([]string, 0, len(capabilities)) for _, capability := range capabilities { if value, found := strings.CutPrefix(capability, prefix); found && value != "" { values = append(values, value) } } sort.Strings(values) return values } func gameClientBridgeFeatureAvailability(features []domain.GameClientBridgeFeatureDeclaration, profiles []domain.GameClientBridgeProfileDeclaration) []domain.GameClientBridgeFeatureAvailability { result := make([]domain.GameClientBridgeFeatureAvailability, 0, len(features)) for _, feature := range features { handlers, producers := map[string]bool{}, map[string]bool{} for _, profile := range profiles { if !profile.Available { continue } for _, handler := range profile.HandlerTypes { handlers[handler] = true } for _, producer := range profile.EventProducerTypes { producers[producer] = true } } missing := make([]string, 0) for _, handler := range feature.RequiredHandlers { if !handlers[handler] { missing = append(missing, "handler."+handler) } } for _, producer := range feature.RequiredEventProducers { if !producers[producer] { missing = append(missing, "event-producer."+producer) } } availability := domain.GameClientBridgeFeatureAvailability{Key: feature.Key, Available: len(missing) == 0} if len(missing) > 0 { availability.Reason = "compatible Companion is missing " + strings.Join(missing, ", ") } result = append(result, availability) } return result } func (svc *CoreService) ListGameClientBridgeCommandsForSession(sessionID string, filter domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error) { if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil { return nil, err } instance, err := svc.store.ServerInstances().Get(filter.ServerInstanceID) if err != nil { return nil, err } filter.PluginID = instance.PluginID commands, err := svc.store.GameClientBridgeCommands().List(filter) if err != nil { return nil, err } result := make([]domain.GameClientBridgeCommand, len(commands)) for index, command := range commands { result[index] = domain.CopyGameClientBridgeCommand(command) } return result, nil } func (svc *CoreService) GetGameClientBridgeCommandForSession(sessionID, commandID string) (domain.GameClientBridgeCommand, error) { command, err := svc.store.GameClientBridgeCommands().Get(commandID) if err != nil { return domain.GameClientBridgeCommand{}, err } if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.CopyGameClientBridgeCommand(command), nil } func (svc *CoreService) QueryGameClientBridgeSnapshotsForSession(sessionID string, query domain.GameClientBridgeSnapshotQuery) ([]domain.GameClientBridgeSnapshot, error) { if err := validator.ValidateGameClientBridgeSnapshotQuery(query); err != nil { return nil, err } if err := svc.authorizeServerLifecycle(sessionID, query.ServerInstanceID); err != nil { return nil, err } instance, err := svc.store.ServerInstances().Get(query.ServerInstanceID) if err != nil { return nil, err } if query.PluginID != instance.PluginID { return nil, ErrForbidden } limit := query.Limit if limit == 0 { limit = 50 } snapshots, err := svc.store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ServerInstanceID: query.ServerInstanceID, PluginID: query.PluginID, ProfileKey: query.ProfileKey, Type: query.Type, StreamKey: query.StreamKey, ObservedAfter: query.ObservedAfter, Limit: limit}) if err != nil { return nil, err } result := make([]domain.GameClientBridgeSnapshot, len(snapshots)) for index, snapshot := range snapshots { result[index] = domain.CopyGameClientBridgeSnapshot(snapshot) } return result, nil } func gameClientBridgeCommandDeclaration(plugin domain.GamePlugin, profileKey, commandType string) (domain.GameClientBridgeCommandDeclaration, bool) { profileDeclared := false for _, profile := range plugin.RuntimeProfiles.ClientManagers { if profile.Key == profileKey && containsString(profile.Health.RequiredCapabilities, gameClientBridgeCapability) { profileDeclared = true break } } if !profileDeclared { return domain.GameClientBridgeCommandDeclaration{}, false } for _, declaration := range plugin.GameClientBridge.Commands { if declaration.Type == commandType { return declaration, true } } return domain.GameClientBridgeCommandDeclaration{}, false } func gameClientBridgeCommandTypes(declarations []domain.GameClientBridgeCommandDeclaration) []string { values := make([]string, len(declarations)) for index, declaration := range declarations { values[index] = declaration.Type } return values } func gameClientBridgeSnapshotTypes(declarations []domain.GameClientBridgeSnapshotDeclaration) []string { seen := map[string]struct{}{} values := make([]string, 0, len(declarations)) for _, declaration := range declarations { if _, exists := seen[declaration.Type]; exists { continue } seen[declaration.Type] = struct{}{} values = append(values, declaration.Type) } return values } func gameClientBridgeQueryTemplateKeys(declarations []domain.GameClientBridgeQueryTemplateDeclaration) []string { values := make([]string, len(declarations)) for index, declaration := range declarations { values[index] = declaration.Key } return values } func (svc *CoreService) queueGameClientBridgeCommand(requesterID string, request domain.GameClientBridgeQueueRequest) (domain.GameClientBridgeCommand, error) { request.Payload = domain.CopyGameClientBridgePayload(request.Payload) if err := validator.ValidateGameClientBridgeQueueRequest(request); err != nil { return domain.GameClientBridgeCommand{}, err } stamp := svc.now() svc.bridgeMu.Lock() defer svc.bridgeMu.Unlock() plugin, err := svc.store.GamePlugins().Get(request.PluginID) if err != nil { return domain.GameClientBridgeCommand{}, err } declaration, declared := gameClientBridgeCommandDeclaration(plugin, request.ProfileKey, request.CommandType) if !declared { return domain.GameClientBridgeCommand{}, validationError("bridge command type is not declared for the profile") } payload, err := json.Marshal(request.Payload) if err != nil || len(payload) > declaration.MaxPayloadBytes { return domain.GameClientBridgeCommand{}, validationError("bridge command payload exceeds declaration") } if request.ExpiresAt.After(stamp.Add(time.Duration(declaration.TimeoutSeconds) * time.Second)) { return domain.GameClientBridgeCommand{}, validationError("bridge command expiry exceeds declared timeout") } existing, err := svc.store.GameClientBridgeCommands().GetByIdempotency(request.ServerInstanceID, requesterID, request.CommandType, request.IdempotencyKey) if err == nil { return domain.CopyGameClientBridgeCommand(existing), nil } if err != nil && err != repo.ErrNotFound { return domain.GameClientBridgeCommand{}, err } if !request.ExpiresAt.After(stamp) { return domain.GameClientBridgeCommand{}, validationError("bridge command expiresAt must be in the future") } svc.bridgeSeq++ command := domain.GameClientBridgeCommand{ ID: fmt.Sprintf("bridge-command-%d-%d", stamp.UnixNano(), svc.bridgeSeq), ServerInstanceID: request.ServerInstanceID, PluginID: request.PluginID, ProfileKey: request.ProfileKey, CommandType: request.CommandType, Payload: domain.CopyGameClientBridgePayload(request.Payload), IdempotencyKey: request.IdempotencyKey, Priority: request.Priority, State: domain.GameClientBridgeCommandPending, RequesterID: requesterID, ExpiresAt: request.ExpiresAt, CreatedAt: stamp, UpdatedAt: stamp, } if err := svc.store.GameClientBridgeCommands().Create(command); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.CopyGameClientBridgeCommand(command), nil } func (svc *CoreService) claimGameClientBridgeCommands(component gameClientBridgeComponentSession, limit int) ([]domain.GameClientBridgeCommand, error) { if limit == 0 { limit = 10 } if limit < 1 || limit > 50 { return nil, validationError("bridge claim limit must be between 1 and 50") } stamp := svc.now() svc.bridgeMu.Lock() defer svc.bridgeMu.Unlock() if err := svc.sweepGameClientBridgeCommandsLocked(stamp); err != nil { return nil, err } commands, err := svc.store.GameClientBridgeCommands().List(domain.GameClientBridgeCommandFilter{ServerInstanceID: component.Session.ServerInstanceID, PluginID: component.Installation.PluginID, ProfileKey: component.Session.ProfileKey, State: domain.GameClientBridgeCommandPending}) if err != nil { return nil, err } sort.SliceStable(commands, func(left, right int) bool { if commands[left].Priority != commands[right].Priority { return commands[left].Priority > commands[right].Priority } if !commands[left].CreatedAt.Equal(commands[right].CreatedAt) { return commands[left].CreatedAt.Before(commands[right].CreatedAt) } return commands[left].ID < commands[right].ID }) claimed := make([]domain.GameClientBridgeCommand, 0, limit) for _, command := range commands { if len(claimed) == limit { break } if command.RunJobID != "" { continue } fencingToken := command.Claim.FencingToken + 1 command.State = domain.GameClientBridgeCommandClaimed command.Claim = domain.GameClientBridgeClaim{SessionID: component.Session.ID, InstallationID: component.Installation.ID, DeploymentGeneration: component.Session.DeploymentGeneration, FencingToken: fencingToken, LeaseExpiresAt: gameClientBridgeClaimLeaseExpiry(stamp, command.ExpiresAt), ClaimedAt: stamp} command.UpdatedAt = stamp if err := svc.store.GameClientBridgeCommands().Update(command); err != nil { return nil, err } claimed = append(claimed, domain.CopyGameClientBridgeCommand(command)) } return claimed, nil } func (svc *CoreService) ackGameClientBridgeCommand(component gameClientBridgeComponentSession, request domain.GameClientBridgeAckRequest) (domain.GameClientBridgeCommand, error) { if err := validator.ValidateGameClientBridgeAckRequest(request); err != nil { return domain.GameClientBridgeCommand{}, err } stamp := svc.now() svc.bridgeMu.Lock() defer svc.bridgeMu.Unlock() command, err := svc.fencedGameClientBridgeCommand(component, request.CommandID, request.FencingToken, stamp) if err != nil { return domain.GameClientBridgeCommand{}, err } command.Claim.AcknowledgedAt = stamp command.Claim.LeaseExpiresAt = gameClientBridgeClaimLeaseExpiry(stamp, command.ExpiresAt) command.UpdatedAt = stamp if err := svc.store.GameClientBridgeCommands().Update(command); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.CopyGameClientBridgeCommand(command), nil } func (svc *CoreService) completeGameClientBridgeCommand(component gameClientBridgeComponentSession, request domain.GameClientBridgeResultRequest) (domain.GameClientBridgeCommand, error) { request.Payload = domain.CopyGameClientBridgePayload(request.Payload) if err := validator.ValidateGameClientBridgeResultRequest(request); err != nil { return domain.GameClientBridgeCommand{}, err } stamp := svc.now() svc.bridgeMu.Lock() defer svc.bridgeMu.Unlock() command, err := svc.store.GameClientBridgeCommands().Get(request.CommandID) if err != nil { return domain.GameClientBridgeCommand{}, err } if isTerminalGameClientBridgeCommandState(command.State) { if command.Result.CompletedBy == component.Session.ID && gameClientBridgeClaimMatches(command, component, request.FencingToken) && command.Result.Status == request.Status && command.Result.Summary == request.Summary && reflect.DeepEqual(command.Result.Payload, request.Payload) { return domain.CopyGameClientBridgeCommand(command), nil } return domain.GameClientBridgeCommand{}, validationError("bridge command already has a terminal result") } command, err = svc.fencedGameClientBridgeCommand(component, request.CommandID, request.FencingToken, stamp) if err != nil { return domain.GameClientBridgeCommand{}, err } switch request.Status { case domain.GameClientBridgeResultSucceeded: command.State = domain.GameClientBridgeCommandSucceeded case domain.GameClientBridgeResultFailed: command.State = domain.GameClientBridgeCommandFailed case domain.GameClientBridgeResultUnknown: command.State = domain.GameClientBridgeCommandUnknown case domain.GameClientBridgeResultCancelled: command.State = domain.GameClientBridgeCommandCancelled } command.Result = domain.GameClientBridgeResult{Status: request.Status, Summary: request.Summary, Payload: domain.CopyGameClientBridgePayload(request.Payload), CompletedBy: component.Session.ID, CompletedAt: stamp} command.CompletedAt = stamp command.UpdatedAt = stamp if err := svc.store.GameClientBridgeCommands().Update(command); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.CopyGameClientBridgeCommand(command), nil } func (svc *CoreService) CancelGameClientBridgeCommandForSession(sessionID string, request domain.GameClientBridgeCancelRequest) (domain.GameClientBridgeCommand, error) { if err := validator.ValidateGameClientBridgeCancelRequest(request); err != nil { return domain.GameClientBridgeCommand{}, err } user, err := svc.GetCurrentUser(sessionID) if err != nil { return domain.GameClientBridgeCommand{}, err } stamp := svc.now() svc.bridgeMu.Lock() defer svc.bridgeMu.Unlock() command, err := svc.store.GameClientBridgeCommands().Get(request.CommandID) if err != nil { return domain.GameClientBridgeCommand{}, err } instance, err := svc.store.ServerInstances().Get(command.ServerInstanceID) if err != nil { return domain.GameClientBridgeCommand{}, err } if !canAccessServer(user, instance) { return domain.GameClientBridgeCommand{}, ErrForbidden } if !isTerminalGameClientBridgeCommandState(command.State) && !command.ExpiresAt.IsZero() && !command.ExpiresAt.After(stamp) { if err := svc.expireGameClientBridgeCommandLocked(command, stamp); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.GameClientBridgeCommand{}, validationError("bridge command expired") } if isTerminalGameClientBridgeCommandState(command.State) { if command.State == domain.GameClientBridgeCommandCancelled { return domain.CopyGameClientBridgeCommand(command), nil } return domain.GameClientBridgeCommand{}, validationError("bridge command is already terminal") } command.State = domain.GameClientBridgeCommandCancelled command.Cancellation = domain.GameClientBridgeCancellation{RequestedBy: user.ID, Reason: request.Reason, CancelledAt: stamp} command.Result = domain.GameClientBridgeResult{Status: domain.GameClientBridgeResultCancelled, Summary: "cancelled by operator", CompletedAt: stamp} command.CompletedAt = stamp command.UpdatedAt = stamp if err := svc.store.GameClientBridgeCommands().Update(command); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.CopyGameClientBridgeCommand(command), nil } func (svc *CoreService) ReconcileGameClientBridgeCommands() error { stamp := svc.now() svc.bridgeMu.Lock() defer svc.bridgeMu.Unlock() if err := svc.sweepGameClientBridgeCommandsLocked(stamp); err != nil { return err } if err := svc.pruneGameClientBridgeCommandsLocked(stamp); err != nil { return err } return svc.pruneGameClientBridgeSnapshotsLocked(stamp) } func (svc *CoreService) pruneGameClientBridgeCommandsLocked(stamp time.Time) error { commands, err := svc.store.GameClientBridgeCommands().List(domain.GameClientBridgeCommandFilter{}) if err != nil { return err } groups := map[string][]domain.GameClientBridgeCommand{} for _, command := range commands { if !isTerminalGameClientBridgeCommandState(command.State) || command.CompletedAt.IsZero() { continue } retention := defaultGameClientBridgeCommandRetention maxRecords := 0 if plugin, pluginErr := svc.store.GamePlugins().Get(command.PluginID); pluginErr == nil { if plugin.GameClientBridge.Retention.KeepForSeconds > 0 { retention = time.Duration(plugin.GameClientBridge.Retention.KeepForSeconds) * time.Second } maxRecords = plugin.GameClientBridge.Retention.MaxRecords } if !command.CompletedAt.After(stamp.Add(-retention)) { if err := svc.store.GameClientBridgeCommands().Delete(command.ID); err != nil { return err } continue } if maxRecords > 0 { key := command.ServerInstanceID + "\x00" + command.PluginID groups[key] = append(groups[key], command) } } for _, group := range groups { if len(group) == 0 { continue } plugin, err := svc.store.GamePlugins().Get(group[0].PluginID) if err != nil || plugin.GameClientBridge.Retention.MaxRecords <= 0 || len(group) <= plugin.GameClientBridge.Retention.MaxRecords { continue } sort.SliceStable(group, func(left, right int) bool { if !group[left].CompletedAt.Equal(group[right].CompletedAt) { return group[left].CompletedAt.After(group[right].CompletedAt) } return group[left].ID < group[right].ID }) for _, command := range group[plugin.GameClientBridge.Retention.MaxRecords:] { if err := svc.store.GameClientBridgeCommands().Delete(command.ID); err != nil { return err } } } return nil } func (svc *CoreService) pruneGameClientBridgeSnapshotsLocked(stamp time.Time) error { snapshots, err := svc.store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{}) if err != nil { return err } groups := map[string][]domain.GameClientBridgeSnapshot{} for _, snapshot := range snapshots { if !snapshot.ExpiresAt.IsZero() && !snapshot.ExpiresAt.After(stamp) { if err := svc.store.GameClientBridgeSnapshots().Delete(snapshot.ID); err != nil { return err } continue } key := snapshot.ServerInstanceID + "\x00" + snapshot.PluginID + "\x00" + snapshot.ProfileKey + "\x00" + snapshot.Type + "\x00" + snapshot.StreamKey groups[key] = append(groups[key], snapshot) } for _, group := range groups { if len(group) == 0 { continue } maxRecords := group[0].Retention.MaxRecords if plugin, pluginErr := svc.store.GamePlugins().Get(group[0].PluginID); pluginErr == nil { if declaration, ok := gameClientBridgeSnapshotDeclaration(plugin, group[0].Type, group[0].SchemaVersion); ok { maxRecords = declaration.Retention.MaxRecords } } if maxRecords <= 0 || len(group) <= maxRecords { continue } sort.SliceStable(group, func(left, right int) bool { if group[left].Sequence != group[right].Sequence { return group[left].Sequence > group[right].Sequence } return group[left].ID < group[right].ID }) for _, snapshot := range group[maxRecords:] { if err := svc.store.GameClientBridgeSnapshots().Delete(snapshot.ID); err != nil { return err } } } return nil } func (svc *CoreService) sweepGameClientBridgeCommandsLocked(stamp time.Time) error { commands, err := svc.store.GameClientBridgeCommands().List(domain.GameClientBridgeCommandFilter{}) if err != nil { return err } for _, command := range commands { if isTerminalGameClientBridgeCommandState(command.State) { continue } if !command.ExpiresAt.IsZero() && !command.ExpiresAt.After(stamp) { if err := svc.expireGameClientBridgeCommandLocked(command, stamp); err != nil { return err } continue } if command.State == domain.GameClientBridgeCommandClaimed && !command.Claim.LeaseExpiresAt.IsZero() && !command.Claim.LeaseExpiresAt.After(stamp) { fencingToken := command.Claim.FencingToken command.State = domain.GameClientBridgeCommandPending command.Claim = domain.GameClientBridgeClaim{FencingToken: fencingToken} command.UpdatedAt = stamp if err := svc.store.GameClientBridgeCommands().Update(command); err != nil { return err } } } return nil } func (svc *CoreService) fencedGameClientBridgeCommand(component gameClientBridgeComponentSession, commandID string, fencingToken uint64, stamp time.Time) (domain.GameClientBridgeCommand, error) { command, err := svc.store.GameClientBridgeCommands().Get(commandID) if err != nil { return domain.GameClientBridgeCommand{}, err } if command.State != domain.GameClientBridgeCommandClaimed || !gameClientBridgeClaimMatches(command, component, fencingToken) { return domain.GameClientBridgeCommand{}, validationError("bridge command claim is stale") } if !command.ExpiresAt.IsZero() && !command.ExpiresAt.After(stamp) { if err := svc.expireGameClientBridgeCommandLocked(command, stamp); err != nil { return domain.GameClientBridgeCommand{}, err } return domain.GameClientBridgeCommand{}, validationError("bridge command expired") } if !command.Claim.LeaseExpiresAt.After(stamp) { return domain.GameClientBridgeCommand{}, validationError("bridge command claim lease expired") } return command, nil } func (svc *CoreService) expireGameClientBridgeCommandLocked(command domain.GameClientBridgeCommand, stamp time.Time) error { if isTerminalGameClientBridgeCommandState(command.State) { return nil } command.State = domain.GameClientBridgeCommandExpired command.CompletedAt = stamp command.UpdatedAt = stamp return svc.store.GameClientBridgeCommands().Update(command) } func gameClientBridgeClaimLeaseExpiry(stamp, commandExpiry time.Time) time.Time { leaseExpiry := stamp.Add(defaultGameClientBridgeLeaseDuration) if !commandExpiry.IsZero() && commandExpiry.Before(leaseExpiry) { return commandExpiry } return leaseExpiry } func gameClientBridgeClaimMatches(command domain.GameClientBridgeCommand, component gameClientBridgeComponentSession, fencingToken uint64) bool { return command.Claim.SessionID == component.Session.ID && command.Claim.InstallationID == component.Installation.ID && command.Claim.DeploymentGeneration == component.Session.DeploymentGeneration && command.Claim.FencingToken == fencingToken } func isTerminalGameClientBridgeCommandState(state domain.GameClientBridgeCommandState) bool { switch state { case domain.GameClientBridgeCommandSucceeded, domain.GameClientBridgeCommandFailed, domain.GameClientBridgeCommandUnknown, domain.GameClientBridgeCommandCancelled, domain.GameClientBridgeCommandExpired: return true default: return false } }