The metadata snapshot deleted every SCUM sqlite query job while persisting, so the recurring database poll lost its job records as soon as anything wrote the snapshot and left its stdout/stderr streams behind. Those polls are the only producer for the platform scum_user and scum_vehicle tables, and the service already retires older terminal polls with their streams, so the snapshot no longer drops them. Startup now removes job log streams whose job no longer exists, keeping the autonomous lifecycle streams the Run posts without a platform job. That clears the streams left by the removed poll producers instead of carrying them in every snapshot.
142 lines
5.0 KiB
Go
142 lines
5.0 KiB
Go
package repo
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"sort"
|
|
"strings"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
func normalizeStoreSnapshot(snapshot StoreSnapshot) StoreSnapshot {
|
|
plugins, replacements := latestGamePlugins(snapshot.GamePlugins)
|
|
snapshot.GamePlugins = plugins
|
|
snapshot.ServerInstances = rewriteSnapshotServerInstances(snapshot.ServerInstances, replacements)
|
|
snapshot.RuntimeBindings = rewriteSnapshotRuntimeBindings(snapshot.RuntimeBindings, replacements)
|
|
snapshot.PluginLifecycles = rewriteSnapshotPluginLifecycles(snapshot.PluginLifecycles, replacements)
|
|
snapshot.AIConfigDiffs = rewriteSnapshotAIConfigDiffs(snapshot.AIConfigDiffs, replacements)
|
|
snapshot.PluginDataRecords = retainedSnapshotPluginData(snapshot.PluginDataRecords, replacements)
|
|
return snapshot
|
|
}
|
|
|
|
func latestGamePlugins(values []domain.GamePlugin) ([]domain.GamePlugin, map[string]domain.GamePlugin) {
|
|
byIdentity := map[string]domain.GamePlugin{}
|
|
for _, plugin := range values {
|
|
key := domain.GamePluginIdentityKey(plugin)
|
|
if current, exists := byIdentity[key]; !exists || domain.GamePluginIsNewer(plugin, current) {
|
|
byIdentity[key] = plugin
|
|
}
|
|
}
|
|
replacements := map[string]domain.GamePlugin{}
|
|
out := make([]domain.GamePlugin, 0, len(byIdentity))
|
|
for _, plugin := range byIdentity {
|
|
out = append(out, domain.CopyGamePlugin(plugin))
|
|
}
|
|
sort.SliceStable(out, func(left, right int) bool { return out[left].ID < out[right].ID })
|
|
for _, plugin := range values {
|
|
kept, ok := byIdentity[domain.GamePluginIdentityKey(plugin)]
|
|
if ok && plugin.ID != kept.ID {
|
|
replacements[plugin.ID] = kept
|
|
}
|
|
}
|
|
return out, replacements
|
|
}
|
|
|
|
func rewriteSnapshotServerInstances(values []domain.ServerInstance, replacements map[string]domain.GamePlugin) []domain.ServerInstance {
|
|
out := make([]domain.ServerInstance, len(values))
|
|
for index, value := range values {
|
|
if kept, ok := replacements[value.PluginID]; ok {
|
|
value.PluginID = kept.ID
|
|
value.PluginVersion = kept.Version
|
|
}
|
|
out[index] = domain.CopyServerInstance(value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func rewriteSnapshotRuntimeBindings(values []domain.RuntimeBinding, replacements map[string]domain.GamePlugin) []domain.RuntimeBinding {
|
|
out := make([]domain.RuntimeBinding, len(values))
|
|
for index, value := range values {
|
|
if kept, ok := replacements[value.PluginID]; ok {
|
|
value.PluginID = kept.ID
|
|
value.PluginVersion = kept.Version
|
|
}
|
|
out[index] = domain.CopyRuntimeBinding(value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func rewriteSnapshotPluginLifecycles(values []domain.PluginLifecycleInstallation, replacements map[string]domain.GamePlugin) []domain.PluginLifecycleInstallation {
|
|
out := make([]domain.PluginLifecycleInstallation, len(values))
|
|
for index, value := range values {
|
|
if kept, ok := replacements[value.PluginID]; ok {
|
|
value.PluginID = kept.ID
|
|
if value.TargetVersion != "" {
|
|
value.TargetVersion = kept.Version
|
|
}
|
|
}
|
|
out[index] = domain.CopyPluginLifecycleInstallation(value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func rewriteSnapshotAIConfigDiffs(values []domain.AIConfigDiffPreview, replacements map[string]domain.GamePlugin) []domain.AIConfigDiffPreview {
|
|
out := make([]domain.AIConfigDiffPreview, len(values))
|
|
for index, value := range values {
|
|
if kept, ok := replacements[value.PluginID]; ok {
|
|
value.PluginID = kept.ID
|
|
}
|
|
out[index] = domain.CopyAIConfigDiffPreview(value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func retainedSnapshotPluginData(values []domain.PluginDataRecord, replacements map[string]domain.GamePlugin) []domain.PluginDataRecord {
|
|
byID := map[string]domain.PluginDataRecord{}
|
|
for _, value := range values {
|
|
if isLegacySCUMPluginData(value.PluginID, value.Collection) {
|
|
continue
|
|
}
|
|
if kept, ok := replacements[value.PluginID]; ok {
|
|
value.PluginID = kept.ID
|
|
value.ID = snapshotPluginDataID(value.ServerInstanceID, kept.ID, value.Collection, value.Key)
|
|
}
|
|
if current, exists := byID[value.ID]; !exists || value.UpdatedAt.After(current.UpdatedAt) || (value.UpdatedAt.Equal(current.UpdatedAt) && value.CreatedAt.After(current.CreatedAt)) {
|
|
byID[value.ID] = domain.CopyPluginDataRecord(value)
|
|
}
|
|
}
|
|
ids := make([]string, 0, len(byID))
|
|
for id := range byID {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
out := make([]domain.PluginDataRecord, 0, len(ids))
|
|
for _, id := range ids {
|
|
out = append(out, domain.CopyPluginDataRecord(byID[id]))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isLegacySCUMPluginData(pluginID string, collection string) bool {
|
|
if !isSCUMPluginID(pluginID) {
|
|
return false
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(collection)) {
|
|
case "scum_users", "scum_players", "scum_trajectories", "scum_user_trajectories", "scum_vehicles", "scum_vehicle_trajectories", "scum_vehicle_locks":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isSCUMPluginID(pluginID string) bool {
|
|
canonical := domain.CanonicalGamePluginID(pluginID)
|
|
return canonical == "game.scum" || canonical == "server.scum"
|
|
}
|
|
|
|
func snapshotPluginDataID(serverID, pluginID, collection, key string) string {
|
|
sum := sha256.Sum256([]byte(serverID + "\x00" + pluginID + "\x00" + collection + "\x00" + key))
|
|
return "plugin-data-" + hex.EncodeToString(sum[:])[:24]
|
|
}
|