gastownhall/beads · warning · ErrMigrationLockUnavailable

schema: acquire migration lock: %w: %w

Error message

schema: acquire migration lock: %w: %w

What it means

GET_LOCK itself errored at the driver level while acquiring the database-scoped schema migration lock. The error wraps both the sentinel ErrMigrationLockUnavailable (making it retryable via IsMigrationLockError) and the underlying driver error.

Source

Thrown at internal/storage/schema/lock.go:352

	var ancestorCount int
	if err := conn.QueryRowContext(ctx,
		"SELECT COUNT(*) FROM dolt_log WHERE commit_hash = ?", c.initialHead,
	).Scan(&ancestorCount); err != nil {
		return false
	}
	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 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat as retryable: check schema.IsMigrationLockError(err) and retry with backoff within your retry budget.
  2. Ensure the caller's context outlives the 5-second lock acquire timeout.
  3. Verify the pinned connection is alive (ping) before attempting migration.
  4. Reduce contention: fewer concurrent initializers, or stagger bd process startup on shared rigs.

Example fix

// before
applied, err := schema.MigrateUpWithLock(ctx, conn, db) // fails on flaky conn
// after
if err := conn.PingContext(ctx); err != nil {
    conn, err = pool.Conn(ctx)
}
applied, err := schema.MigrateUpWithLock(ctx, conn, db)
if schema.IsMigrationLockError(err) { /* retry with backoff */ }
Defensive patterns

Strategy: retry

Validate before calling

if err := conn.PingContext(ctx); err != nil { return err }
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) // must exceed the 5s lock timeout
defer cancel()

Type guard

func isLockUnavailable(err error) bool { return errors.Is(err, schema.ErrMigrationLockUnavailable) }

Try / catch

applied, err := schema.MigrateUpWithLock(ctx, conn, db)
if schema.IsMigrationLockError(err) {
    time.Sleep(backoff)
    return retryOpen(ctx) // retry with exponential backoff, within retry budget
}

Prevention

When it happens

Trigger: conn.QueryRowContext("SELECT GET_LOCK(?, ?)") fails: connection broken/closed, context cancelled or deadline exceeded during the up-to-5s lock wait, server restart.

Common situations: Heavily contended shared Dolt server (many bd processes); idle connection reaped by the server; caller's ctx deadline shorter than the 5-second lock timeout.

Related errors


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