343 lines
18 KiB
Go
343 lines
18 KiB
Go
package repo
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
)
|
|
|
|
const mysqlSnapshotID = "current"
|
|
|
|
type MySQLStore struct {
|
|
*MemoryStore
|
|
db *sql.DB
|
|
persistMu sync.Mutex
|
|
}
|
|
|
|
func NewMySQLStore(dsn string) (*MySQLStore, error) {
|
|
dsn = strings.TrimSpace(dsn)
|
|
if dsn == "" {
|
|
return nil, fmt.Errorf("PLATFORM_MYSQL_DSN is required when PLATFORM_STORAGE_BACKEND=mysql")
|
|
}
|
|
db, err := sql.Open("mysql", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open mysql metadata store: %w", err)
|
|
}
|
|
store := &MySQLStore{
|
|
MemoryStore: NewMemoryStore(),
|
|
db: db,
|
|
}
|
|
if err := store.initialize(); err != nil {
|
|
_ = db.Close()
|
|
return nil, err
|
|
}
|
|
if err := store.load(); err != nil {
|
|
_ = db.Close()
|
|
return nil, err
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
func (store *MySQLStore) Close() error {
|
|
return store.db.Close()
|
|
}
|
|
|
|
func (store *MySQLStore) Users() UserRepository {
|
|
return &persistentRepository[domain.User, domain.UserFilter]{repository: store.MemoryStore.users, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) AuthSessions() AuthSessionRepository {
|
|
return &persistentRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]{repository: store.MemoryStore.authSessions, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) RunControlSessions() RunControlSessionRepository {
|
|
return &persistentRepository[domain.RunControlSession, struct{}]{repository: store.MemoryStore.runSessions, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) AIProviders() AIProviderRepository {
|
|
return &persistentRepository[domain.AIProvider, domain.AIProviderFilter]{repository: store.MemoryStore.aiProviders, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) GamePlugins() GamePluginRepository {
|
|
return &persistentRepository[domain.GamePlugin, domain.GamePluginFilter]{repository: store.MemoryStore.gamePlugins, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) ServerInstances() ServerInstanceRepository {
|
|
return &persistentRepository[domain.ServerInstance, domain.ServerInstanceFilter]{repository: store.MemoryStore.serverInstances, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) RunEndpoints() RunEndpointRepository {
|
|
return &persistentRepository[domain.RunEndpoint, domain.RunEndpointFilter]{repository: store.MemoryStore.runEndpoints, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) Jobs() JobRepository {
|
|
return &persistentJobRepository{
|
|
persistentRepository: &persistentRepository[domain.Job, domain.JobFilter]{repository: store.MemoryStore.jobs, persist: store.persist},
|
|
repository: store.MemoryStore.jobs,
|
|
}
|
|
}
|
|
|
|
func (store *MySQLStore) Artifacts() ArtifactRepository {
|
|
return &persistentRepository[domain.Artifact, domain.ArtifactFilter]{repository: store.MemoryStore.artifacts, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) RuntimeBindings() RuntimeBindingRepository {
|
|
return &persistentRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]{repository: store.MemoryStore.runtimeBindings, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) EncryptedComponentKeys() EncryptedComponentKeyRepository {
|
|
return &persistentRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]{repository: store.MemoryStore.componentKeys, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) RunDistributions() RunDistributionRepository {
|
|
return &persistentRepository[domain.RunDistribution, domain.RunDistributionFilter]{repository: store.MemoryStore.runDists, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) ClientManagerDistributions() ClientManagerDistributionRepository {
|
|
return &persistentRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]{repository: store.MemoryStore.clientDists, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) ClientManagerInstallations() ClientManagerInstallationRepository {
|
|
return &persistentRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]{repository: store.MemoryStore.clientInstalls, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) ClientManagerSessions() ClientManagerSessionRepository {
|
|
return &persistentRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]{repository: store.MemoryStore.clientSessions, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) ClientManagerNonces() ClientManagerNonceRepository {
|
|
return &persistentRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]{repository: store.MemoryStore.clientNonces, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) DependencyStatuses() DependencyStatusRepository {
|
|
return &persistentRepository[domain.DependencyStatus, domain.DependencyStatusFilter]{repository: store.MemoryStore.dependencies, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
|
|
return &persistentRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]{repository: store.MemoryStore.buildJobs, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) RunUpdateJobs() RunUpdateJobRepository {
|
|
return &persistentRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]{repository: store.MemoryStore.updateJobs, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) LogStreams() LogStreamRepository {
|
|
return &persistentRepository[domain.LogStream, domain.LogStreamFilter]{repository: store.MemoryStore.logStreams, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) AuditEvents() AuditEventRepository {
|
|
return &persistentRepository[domain.AuditEvent, domain.AuditEventFilter]{repository: store.MemoryStore.auditEvents, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) MetricSamples() MetricSampleRepository {
|
|
return &persistentRepository[domain.MetricSample, domain.MetricSampleFilter]{repository: store.MemoryStore.metricSamples, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) Backups() BackupRepository {
|
|
return &persistentRepository[domain.BackupRecord, domain.BackupFilter]{repository: store.MemoryStore.backups, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) Alerts() AlertRepository {
|
|
return &persistentRepository[domain.AlertRecord, domain.AlertFilter]{repository: store.MemoryStore.alerts, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) PluginLifecycles() PluginLifecycleRepository {
|
|
return &persistentRepository[domain.PluginLifecycleInstallation, domain.PluginLifecycleFilter]{repository: store.MemoryStore.pluginLifecycle, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) AIConfigDiffs() AIConfigDiffRepository {
|
|
return &persistentRepository[domain.AIConfigDiffPreview, domain.AIConfigDiffFilter]{repository: store.MemoryStore.aiConfigDiffs, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) GameClientBridgeCommands() GameClientBridgeCommandRepository {
|
|
return &persistentGameClientBridgeCommandRepository{
|
|
persistentRepository: &persistentRepository[domain.GameClientBridgeCommand, domain.GameClientBridgeCommandFilter]{repository: store.MemoryStore.bridgeCommands, persist: store.persist},
|
|
repository: store.MemoryStore.bridgeCommands,
|
|
}
|
|
}
|
|
|
|
func (store *MySQLStore) GameClientBridgeSnapshots() GameClientBridgeSnapshotRepository {
|
|
return &persistentRepository[domain.GameClientBridgeSnapshot, domain.GameClientBridgeSnapshotFilter]{repository: store.MemoryStore.bridgeSnapshots, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) GameClientBridgeSnapshotStreams() GameClientBridgeSnapshotStreamRepository {
|
|
return &persistentRepository[domain.GameClientBridgeSnapshotStream, domain.GameClientBridgeSnapshotStreamFilter]{repository: store.MemoryStore.bridgeStreams, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GamePlayers() GamePlayerRepository {
|
|
return &persistentRepository[domain.GamePlayer, domain.GamePlayerFilter]{repository: store.MemoryStore.gamePlayers, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GamePlayerAliases() GamePlayerAliasRepository {
|
|
return &persistentRepository[domain.GamePlayerAlias, domain.GamePlayerAliasFilter]{repository: store.MemoryStore.gamePlayerAliases, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GamePlayerSessions() GamePlayerSessionRepository {
|
|
return &persistentRepository[domain.GamePlayerSession, domain.GamePlayerSessionFilter]{repository: store.MemoryStore.gamePlayerSessions, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GameAccessAttempts() GameAccessAttemptRepository {
|
|
return &persistentRepository[domain.GameAccessAttempt, domain.GameAccessAttemptFilter]{repository: store.MemoryStore.gameAccessAttempts, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GameSecuritySignals() GameSecuritySignalRepository {
|
|
return &persistentRepository[domain.GameSecuritySignal, domain.GameSecuritySignalFilter]{repository: store.MemoryStore.gameSecuritySignals, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GamePlayerStatePatches() GamePlayerStatePatchRepository {
|
|
return &persistentRepository[domain.GamePlayerStatePatch, domain.GamePlayerStatePatchFilter]{repository: store.MemoryStore.gamePlayerStatePatches, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GameMapTrackPoints() GameMapTrackPointRepository {
|
|
return &persistentRepository[domain.GameMapTrackPoint, domain.GameMapTrackPointFilter]{repository: store.MemoryStore.gameMapTrackPoints, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GamePlayerVehicleSegments() GamePlayerVehicleSegmentRepository {
|
|
return &persistentRepository[domain.GamePlayerVehicleSegment, domain.GamePlayerVehicleSegmentFilter]{repository: store.MemoryStore.gamePlayerVehicleSegments, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GameGiftCatalogs() GameGiftCatalogRepository {
|
|
return &persistentRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]{repository: store.MemoryStore.gameGiftCatalogs, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GameGiftRevisions() GameGiftRevisionRepository {
|
|
return &persistentRepository[domain.GameGiftRevision, domain.GameGiftRevisionFilter]{repository: store.MemoryStore.gameGiftRevisions, persist: store.persist}
|
|
}
|
|
func (store *MySQLStore) GameGiftGrants() GameGiftGrantRepository {
|
|
return &persistentRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]{repository: store.MemoryStore.gameGiftGrants, persist: store.persist}
|
|
}
|
|
|
|
func (store *MySQLStore) initialize() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := store.db.PingContext(ctx); err != nil {
|
|
return fmt.Errorf("connect mysql metadata store: %w", err)
|
|
}
|
|
_, err := store.db.ExecContext(ctx, `
|
|
CREATE TABLE IF NOT EXISTS platform_metadata_snapshots (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
snapshot_json JSON NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)`)
|
|
if err != nil {
|
|
return fmt.Errorf("create mysql metadata snapshot table: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *MySQLStore) load() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
var payload []byte
|
|
err := store.db.QueryRowContext(ctx, "SELECT snapshot_json FROM platform_metadata_snapshots WHERE id = ?", mysqlSnapshotID).Scan(&payload)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("read mysql metadata snapshot: %w", err)
|
|
}
|
|
var snapshot StoreSnapshot
|
|
if err := json.Unmarshal(payload, &snapshot); err != nil {
|
|
return fmt.Errorf("decode mysql metadata snapshot: %w", err)
|
|
}
|
|
store.loadSnapshot(snapshot)
|
|
return nil
|
|
}
|
|
|
|
func (store *MySQLStore) persist() error {
|
|
store.persistMu.Lock()
|
|
defer store.persistMu.Unlock()
|
|
|
|
snapshot := store.snapshot()
|
|
payload, err := json.Marshal(snapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("encode mysql metadata snapshot: %w", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, err = store.db.ExecContext(ctx, `
|
|
INSERT INTO platform_metadata_snapshots (id, snapshot_json)
|
|
VALUES (?, ?)
|
|
ON DUPLICATE KEY UPDATE snapshot_json = ?, updated_at = CURRENT_TIMESTAMP`, mysqlSnapshotID, string(payload), string(payload))
|
|
if err != nil {
|
|
return fmt.Errorf("write mysql metadata snapshot: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *MySQLStore) snapshot() StoreSnapshot {
|
|
return StoreSnapshot{
|
|
Users: snapshotRepository(store.MemoryStore.users),
|
|
AuthSessions: snapshotRepository(store.MemoryStore.authSessions),
|
|
RunControlSessions: snapshotRepository(store.MemoryStore.runSessions),
|
|
AIProviders: snapshotRepository(store.MemoryStore.aiProviders),
|
|
GamePlugins: snapshotRepository(store.MemoryStore.gamePlugins),
|
|
ServerInstances: snapshotRepository(store.MemoryStore.serverInstances),
|
|
RunEndpoints: snapshotRepository(store.MemoryStore.runEndpoints),
|
|
Jobs: snapshotRepository(store.MemoryStore.jobs.memoryRepository),
|
|
Artifacts: snapshotRepository(store.MemoryStore.artifacts),
|
|
RuntimeBindings: snapshotRepository(store.MemoryStore.runtimeBindings),
|
|
EncryptedComponentKeys: snapshotRepository(store.MemoryStore.componentKeys),
|
|
RunDistributions: snapshotRepository(store.MemoryStore.runDists),
|
|
ClientManagerDistributions: snapshotRepository(store.MemoryStore.clientDists),
|
|
ClientManagerInstallations: snapshotRepository(store.MemoryStore.clientInstalls),
|
|
ClientManagerSessions: snapshotRepository(store.MemoryStore.clientSessions),
|
|
ClientManagerNonces: snapshotRepository(store.MemoryStore.clientNonces),
|
|
DependencyStatuses: snapshotRepository(store.MemoryStore.dependencies),
|
|
ClientManagerBuildJobs: snapshotRepository(store.MemoryStore.buildJobs),
|
|
RunUpdateJobs: snapshotRepository(store.MemoryStore.updateJobs),
|
|
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
|
|
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
|
|
MetricSamples: snapshotRepository(store.MemoryStore.metricSamples),
|
|
Backups: snapshotRepository(store.MemoryStore.backups),
|
|
Alerts: snapshotRepository(store.MemoryStore.alerts),
|
|
PluginLifecycles: snapshotRepository(store.MemoryStore.pluginLifecycle),
|
|
AIConfigDiffs: snapshotRepository(store.MemoryStore.aiConfigDiffs),
|
|
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
|
|
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
|
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
|
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
|
|
}
|
|
}
|
|
|
|
func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
|
|
loadRepository(store.MemoryStore.users, snapshot.Users)
|
|
loadRepository(store.MemoryStore.authSessions, snapshot.AuthSessions)
|
|
loadRepository(store.MemoryStore.runSessions, snapshot.RunControlSessions)
|
|
loadRepository(store.MemoryStore.aiProviders, snapshot.AIProviders)
|
|
loadRepository(store.MemoryStore.gamePlugins, snapshot.GamePlugins)
|
|
loadRepository(store.MemoryStore.serverInstances, snapshot.ServerInstances)
|
|
loadRepository(store.MemoryStore.runEndpoints, snapshot.RunEndpoints)
|
|
loadRepository(store.MemoryStore.jobs.memoryRepository, snapshot.Jobs)
|
|
loadRepository(store.MemoryStore.artifacts, snapshot.Artifacts)
|
|
loadRepository(store.MemoryStore.runtimeBindings, snapshot.RuntimeBindings)
|
|
loadRepository(store.MemoryStore.componentKeys, snapshot.EncryptedComponentKeys)
|
|
loadRepository(store.MemoryStore.runDists, snapshot.RunDistributions)
|
|
loadRepository(store.MemoryStore.clientDists, snapshot.ClientManagerDistributions)
|
|
loadRepository(store.MemoryStore.clientInstalls, snapshot.ClientManagerInstallations)
|
|
loadRepository(store.MemoryStore.clientSessions, snapshot.ClientManagerSessions)
|
|
loadRepository(store.MemoryStore.clientNonces, snapshot.ClientManagerNonces)
|
|
loadRepository(store.MemoryStore.dependencies, snapshot.DependencyStatuses)
|
|
loadRepository(store.MemoryStore.buildJobs, snapshot.ClientManagerBuildJobs)
|
|
loadRepository(store.MemoryStore.updateJobs, snapshot.RunUpdateJobs)
|
|
loadRepository(store.MemoryStore.logStreams, snapshot.LogStreams)
|
|
loadRepository(store.MemoryStore.auditEvents, snapshot.AuditEvents)
|
|
loadRepository(store.MemoryStore.metricSamples, snapshot.MetricSamples)
|
|
loadRepository(store.MemoryStore.backups, snapshot.Backups)
|
|
loadRepository(store.MemoryStore.alerts, snapshot.Alerts)
|
|
loadRepository(store.MemoryStore.pluginLifecycle, snapshot.PluginLifecycles)
|
|
loadRepository(store.MemoryStore.aiConfigDiffs, snapshot.AIConfigDiffs)
|
|
loadRepository(store.MemoryStore.bridgeCommands.memoryRepository, snapshot.GameClientBridgeCommands)
|
|
loadRepository(store.MemoryStore.bridgeSnapshots.memoryRepository, snapshot.GameClientBridgeSnapshots)
|
|
loadRepository(store.MemoryStore.bridgeStreams, snapshot.GameClientBridgeStreams)
|
|
loadRepository(store.MemoryStore.gamePlayers, snapshot.GamePlayers)
|
|
loadRepository(store.MemoryStore.gamePlayerAliases, snapshot.GamePlayerAliases)
|
|
loadRepository(store.MemoryStore.gamePlayerSessions, snapshot.GamePlayerSessions)
|
|
loadRepository(store.MemoryStore.gameAccessAttempts, snapshot.GameAccessAttempts)
|
|
loadRepository(store.MemoryStore.gameSecuritySignals, snapshot.GameSecuritySignals)
|
|
loadRepository(store.MemoryStore.gamePlayerStatePatches, snapshot.GamePlayerStatePatches)
|
|
loadRepository(store.MemoryStore.gameMapTrackPoints, snapshot.GameMapTrackPoints)
|
|
loadRepository(store.MemoryStore.gamePlayerVehicleSegments, snapshot.GamePlayerVehicleSegments)
|
|
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
|
|
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
|
|
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
|
|
}
|