first commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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) 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) 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) 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),
|
||||
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),
|
||||
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
|
||||
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.users, snapshot.Users)
|
||||
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.logStreams, snapshot.LogStreams)
|
||||
loadRepository(store.MemoryStore.auditEvents, snapshot.AuditEvents)
|
||||
}
|
||||
Reference in New Issue
Block a user