Remove legacy client-manager workflows

This commit is contained in:
npc0-hue
2026-09-03 13:08:08 +08:00
parent bf3c382d15
commit fe09d21a56
56 changed files with 304 additions and 4121 deletions
+51 -194
View File
@@ -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: