package data import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "fmt" "time" configpkg "kra/internal/config" "gorm.io/gorm" ) // acquireInitializationLock serializes first-install work across backend Pods. // The Kubernetes deployment uses MySQL, whose named lock is connection-scoped // and therefore remains held while the candidate database is migrated and seeded. func acquireInitializationLock(ctx context.Context, db *gorm.DB, database *configpkg.Database) (func(), error) { if normalizedDriver(database.Driver) != "mysql" { return func() {}, nil } sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("open initialization lock connection: %w", err) } conn, err := sqlDB.Conn(ctx) if err != nil { return nil, fmt.Errorf("reserve initialization lock connection: %w", err) } lockName := initializationLockName(database) var acquired sql.NullInt64 if err := conn.QueryRowContext(ctx, "SELECT GET_LOCK(?, ?)", lockName, 60).Scan(&acquired); err != nil { _ = conn.Close() return nil, fmt.Errorf("acquire database initialization lock: %w", err) } if !acquired.Valid || acquired.Int64 != 1 { _ = conn.Close() return nil, fmt.Errorf("database initialization lock is busy") } return func() { releaseCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) _, _ = conn.ExecContext(releaseCtx, "SELECT RELEASE_LOCK(?)", lockName) cancel() _ = conn.Close() }, nil } func initializationLockName(database *configpkg.Database) string { identity := fmt.Sprintf("%s\x00%s\x00%s\x00%s", database.Driver, database.Host, database.Port, database.Name) sum := sha256.Sum256([]byte(identity)) return "kra-init-" + hex.EncodeToString(sum[:])[:48] } func persistedDatabaseConfigured(runtime *configpkg.Store) bool { if runtime == nil || runtime.ConfigPath() == "" { return false } persisted, err := configpkg.Load(runtime.ConfigPath()) if err != nil || persisted == nil || persisted.Data == nil { return false } return databaseConnectionConfigured(persisted.Data.Database) }