Remove legacy client-manager workflows
This commit is contained in:
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,15 +12,8 @@ import (
|
||||
"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 {
|
||||
@@ -56,40 +48,20 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
|
||||
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
|
||||
status := domain.GameClientBridgeStatus{ServerInstanceID: instance.ID, PluginID: plugin.ID, Reason: "plugin does not declare a game client bridge", Profiles: []domain.GameClientBridgeProfileDeclaration{}, Features: []domain.GameClientBridgeFeatureAvailability{}}
|
||||
if !gameClientBridgeDeclared(plugin.GameClientBridge) {
|
||||
return domain.CopyGameClientBridgeStatus(status), nil
|
||||
}
|
||||
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"
|
||||
commandTypes := gameClientBridgeCommandTypes(plugin.GameClientBridge.Commands)
|
||||
snapshotTypes := gameClientBridgeSnapshotTypes(plugin.GameClientBridge.Snapshots)
|
||||
declaration := domain.GameClientBridgeProfileDeclaration{PluginID: plugin.ID, ProfileKey: "plugin-owned", Available: true, CommandTypes: commandTypes, SnapshotTypes: snapshotTypes, QueryTemplateKeys: gameClientBridgeQueryTemplateKeys(plugin.GameClientBridge.QueryTemplates), HandlerTypes: commandTypes, EventProducerTypes: snapshotTypes}
|
||||
if ready, reason := svc.gameClientBridgeRuntimeReadiness(instance, plugin); !ready {
|
||||
declaration.Available = false
|
||||
declaration.Reason = reason
|
||||
status.Reason = reason
|
||||
}
|
||||
status.Profiles = append(status.Profiles, declaration)
|
||||
status.Available = declaration.Available
|
||||
if status.Available {
|
||||
status.Reason = ""
|
||||
}
|
||||
@@ -97,15 +69,37 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
|
||||
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)
|
||||
func gameClientBridgeDeclared(bridge domain.GameClientBridgeManifest) bool {
|
||||
return len(bridge.Commands) > 0 || len(bridge.Snapshots) > 0 || len(bridge.QueryTemplates) > 0 || len(bridge.LifecycleProjections) > 0 || len(bridge.DataPacks) > 0 || len(bridge.Pages) > 0 || len(bridge.Features) > 0
|
||||
}
|
||||
|
||||
func (svc *CoreService) gameClientBridgeRuntimeReadiness(instance domain.ServerInstance, plugin domain.GamePlugin) (bool, string) {
|
||||
if len(plugin.GameClientBridge.QueryTemplates) == 0 {
|
||||
return true, ""
|
||||
}
|
||||
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return false, "Run endpoint is not registered"
|
||||
}
|
||||
if endpoint.Status == domain.RunEndpointStatusOffline || endpoint.Status == domain.RunEndpointStatusDisabled {
|
||||
return false, "Run heartbeat has not been observed"
|
||||
}
|
||||
transports := map[string]domain.RuntimeTransportProfile{}
|
||||
for _, transport := range plugin.RuntimeProfiles.TransportProfiles {
|
||||
transports[transport.Key] = transport
|
||||
}
|
||||
for _, template := range plugin.GameClientBridge.QueryTemplates {
|
||||
transport, ok := transports[template.TransportKey]
|
||||
if !ok {
|
||||
return false, "bridge query transport is not declared"
|
||||
}
|
||||
for _, capability := range transport.Capabilities {
|
||||
if !svc.endpointSupports(endpoint, capability) {
|
||||
return false, "Run endpoint is missing bridge transport capability"
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func gameClientBridgeFeatureAvailability(features []domain.GameClientBridgeFeatureDeclaration, profiles []domain.GameClientBridgeProfileDeclaration) []domain.GameClientBridgeFeatureAvailability {
|
||||
@@ -204,14 +198,7 @@ func (svc *CoreService) QueryGameClientBridgeSnapshotsForSession(sessionID strin
|
||||
}
|
||||
|
||||
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 {
|
||||
if strings.TrimSpace(profileKey) == "" {
|
||||
return domain.GameClientBridgeCommandDeclaration{}, false
|
||||
}
|
||||
for _, declaration := range plugin.GameClientBridge.Commands {
|
||||
@@ -243,6 +230,15 @@ func gameClientBridgeSnapshotTypes(declarations []domain.GameClientBridgeSnapsho
|
||||
return values
|
||||
}
|
||||
|
||||
func gameClientBridgeSnapshotDeclaration(plugin domain.GamePlugin, snapshotType, schemaVersion string) (domain.GameClientBridgeSnapshotDeclaration, bool) {
|
||||
for _, declaration := range plugin.GameClientBridge.Snapshots {
|
||||
if declaration.Type == snapshotType && declaration.SchemaVersion == schemaVersion {
|
||||
return declaration, true
|
||||
}
|
||||
}
|
||||
return domain.GameClientBridgeSnapshotDeclaration{}, false
|
||||
}
|
||||
|
||||
func gameClientBridgeQueryTemplateKeys(declarations []domain.GameClientBridgeQueryTemplateDeclaration) []string {
|
||||
values := make([]string, len(declarations))
|
||||
for index, declaration := range declarations {
|
||||
@@ -306,113 +302,6 @@ func (svc *CoreService) queueGameClientBridgeCommand(requesterID string, request
|
||||
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
|
||||
@@ -595,26 +484,6 @@ func (svc *CoreService) sweepGameClientBridgeCommandsLocked(stamp time.Time) 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
|
||||
@@ -625,18 +494,6 @@ func (svc *CoreService) expireGameClientBridgeCommandLocked(command domain.GameC
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user