gastownhall/beads · error · ErrMigrationLockUnavailable
schema: acquire migration lock: %w: timeout
Error message
schema: acquire migration lock: %w: timeout
What it means
AcquireMigrationLock wraps MySQL/Dolt GET_LOCK: the call returned 0, meaning the named session lock was not acquired within migrationLockAcquireTimeoutSeconds because another session holds it. The library throws this to prevent concurrent schema migrations against the same database. The sentinel ErrMigrationLockUnavailable is wrapped so callers can test with errors.Is.
Source
Thrown at internal/storage/schema/lock.go:358
if ancestorCount != 1 {
return false
}
return c.consumed.CompareAndSwap(false, true)
}
// AcquireMigrationLock acquires the named schema migration lock on the pinned
// connection's current Dolt/MySQL session.
func AcquireMigrationLock(ctx context.Context, conn *sql.Conn, lockName string) error {
var locked sql.NullInt64
if err := conn.QueryRowContext(ctx, "SELECT GET_LOCK(?, ?)", lockName, migrationLockAcquireTimeoutSeconds).Scan(&locked); err != nil {
return fmt.Errorf("schema: acquire migration lock: %w: %w", ErrMigrationLockUnavailable, err)
}
if !locked.Valid {
return fmt.Errorf("schema: acquire migration lock: %w: returned NULL", ErrMigrationLockUnavailable)
}
if locked.Int64 != 1 {
return fmt.Errorf("schema: acquire migration lock: %w: timeout", ErrMigrationLockUnavailable)
}
return nil
}
// ReleaseMigrationLock releases the named schema migration lock from the same
// pinned Dolt/MySQL session used to acquire it.
func ReleaseMigrationLock(conn *sql.Conn, lockName string) error {
cleanupCtx, cancel := context.WithTimeout(context.Background(), migrationLockCleanupTimeout)
defer cancel()
var released sql.NullInt64
if err := conn.QueryRowContext(cleanupCtx, "SELECT RELEASE_LOCK(?)", lockName).Scan(&released); err != nil {
discardConn(conn)
return fmt.Errorf("schema: release migration lock: %w: %w", ErrMigrationLockRelease, err)
}
if !released.Valid {
discardConn(conn)
return fmt.Errorf("schema: release migration lock: %w: returned NULL", ErrMigrationLockRelease)View on GitHub (pinned to 71377f2769)
Solutions
- Identify the holder: check for other bd/migration processes on the same database and wait for or stop them.
- Reconnect: the lock is session-scoped — killing or recycling the holding connection releases it (on Dolt, restart the server or close the stale session).
- Retry the migration after the current holder finishes; the timeout is bounded by migrationLockAcquireTimeoutSeconds.
- Verify only one migration runner is configured (e.g. avoid running bd doctor/migrate in parallel cron jobs).
Example fix
// before: retry immediately and fail
if err := schema.AcquireMigrationLock(ctx, conn, lockName); err != nil {
return err
}
// after: detect contention and back off/retry
if err := schema.AcquireMigrationLock(ctx, conn, lockName); err != nil {
if errors.Is(err, schema.ErrMigrationLockUnavailable) {
time.Sleep(retryDelay)
return retryAcquire(ctx, conn, lockName)
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
// Check no other holder before migrating
var held sql.NullInt64
_ = conn.QueryRowContext(ctx, "SELECT IS_FREE_LOCK(?)", lockName).Scan(&held)
if held.Valid && held.Int64 != 1 { /* lock busy: defer or wait */ } Try / catch
if err := schema.AcquireMigrationLock(ctx, conn, name); err != nil {
if errors.Is(err, schema.ErrMigrationLockUnavailable) {
// back off and retry; another migration is running
}
return err
} Prevention
- Run migrations from a single process/leader (use an advisory or scheduler lock).
- Never run bd migrate concurrently in cron/CI jobs against the same database.
- Retry with backoff on ErrMigrationLockUnavailable instead of failing immediately.
- Monitor for stale sessions holding GET_LOCK after crashes.
When it happens
Trigger: Calling AcquireMigrationLock (typically via MigrateUpWithLock) when another process/session already holds GET_LOCK on the same lockName, and the wait timeout expires; also when GET_LOCK itself errors (wrapped err) or returns NULL (e.g. on replicas or unsupported backends).
Common situations: Two bd processes (or an agent and a human) running migrations concurrently; a crashed process whose session still holds the lock; a long-running migration blocking a second one; a stale connection left holding the lock from a prior failed run.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- failed to migrate credential keys: %w
- failed to update encrypted password for peer %s: %w
- %w The Dolt database is locked.%s Try: bd doctor --fix (cle
- failed to initialize schema: %w
- failed to rebuild pool after migration: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/aced43467c198934.
Report an issue: GitHub.