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

  1. Identify the holder: check for other bd/migration processes on the same database and wait for or stop them.
  2. Reconnect: the lock is session-scoped — killing or recycling the holding connection releases it (on Dolt, restart the server or close the stale session).
  3. Retry the migration after the current holder finishes; the timeout is bounded by migrationLockAcquireTimeoutSeconds.
  4. 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

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

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/aced43467c198934. Report an issue: GitHub.