fix(multiplayer): repair allocated compose verification

This commit is contained in:
Josh Creek
2026-09-04 16:38:10 +01:00
parent e6733bd6cb
commit d64920b0f9
11 changed files with 150 additions and 48 deletions
+32 -6
View File
@@ -15,6 +15,32 @@ const migrationTableSQL = `CREATE TABLE IF NOT EXISTS schema_migrations (
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`
const migrationLockSQL = `SELECT pg_advisory_xact_lock(hashtext('cosmic-clash:migrations'))`
// ensureMigrationTable serializes the bootstrap DDL itself. PostgreSQL's
// CREATE TABLE IF NOT EXISTS is not safe against concurrent first creation:
// the relation-type catalog entry can still collide before either statement
// observes the other table. Every long-lived role calls Apply at startup, so
// take the same transaction-scoped advisory lock used for individual files
// before issuing the bootstrap statement.
func ensureMigrationTable(ctx context.Context, db *sql.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin migration bootstrap: %w", err)
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, migrationLockSQL); err != nil {
return fmt.Errorf("lock migration bootstrap: %w", err)
}
if _, err := tx.ExecContext(ctx, migrationTableSQL); err != nil {
return fmt.Errorf("create migration table: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit migration bootstrap: %w", err)
}
return nil
}
// Apply executes numbered SQL files in lexical order. A transaction-level
// advisory lock serializes concurrent API/worker starts, while each migration
// is committed together with its schema_migrations marker so a failed
@@ -31,8 +57,8 @@ func Apply(ctx context.Context, db *sql.DB, directory string) error {
if len(paths) == 0 {
return fmt.Errorf("no migrations found in %s", directory)
}
if _, err := db.ExecContext(ctx, migrationTableSQL); err != nil {
return fmt.Errorf("create migration table: %w", err)
if err := ensureMigrationTable(ctx, db); err != nil {
return err
}
for _, path := range paths {
version := filepath.Base(path)
@@ -50,7 +76,7 @@ func Apply(ctx context.Context, db *sql.DB, directory string) error {
_ = tx.Rollback()
}
}()
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(hashtext('cosmic-clash:migrations'))`); err != nil {
if _, err := tx.ExecContext(ctx, migrationLockSQL); err != nil {
return fmt.Errorf("lock migration %s: %w", version, err)
}
var applied bool
@@ -86,8 +112,8 @@ func Rollback(ctx context.Context, db *sql.DB, directory string, steps int) erro
if db == nil || strings.TrimSpace(directory) == "" || steps <= 0 {
return fmt.Errorf("database, migration directory and a positive step count are required")
}
if _, err := db.ExecContext(ctx, migrationTableSQL); err != nil {
return fmt.Errorf("create migration table: %w", err)
if err := ensureMigrationTable(ctx, db); err != nil {
return err
}
rows, err := db.QueryContext(ctx, `SELECT version FROM schema_migrations ORDER BY version DESC LIMIT $1`, steps)
if err != nil {
@@ -123,7 +149,7 @@ func Rollback(ctx context.Context, db *sql.DB, directory string, steps int) erro
_ = tx.Rollback()
}
}()
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(hashtext('cosmic-clash:migrations'))`); err != nil {
if _, err := tx.ExecContext(ctx, migrationLockSQL); err != nil {
return fmt.Errorf("lock rollback %s: %w", version, err)
}
var applied bool