gastownhall/beads · warning · ErrMigrationLockRelease
schema: release migration lock: %w: returned %d
Error message
schema: release migration lock: %w: returned %d
What it means
RELEASE_LOCK returned a value other than 1 (and not NULL), e.g. 0, meaning the lock exists but is held by a different session. The library wraps ErrMigrationLockRelease with the returned value and discards the connection. Like the NULL case, it flags a broken acquire/release pairing.
Source
Thrown at internal/storage/schema/lock.go:380
// 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)
}
if released.Int64 != 1 {
discardConn(conn)
return fmt.Errorf("schema: release migration lock: %w: returned %d", ErrMigrationLockRelease, released.Int64)
}
return nil
}
func discardConn(conn *sql.Conn) {
_ = conn.Raw(func(driverConn any) error {
return driver.ErrBadConn
})
}
View on GitHub (pinned to 71377f2769)
Solutions
- Verify the release lockName exactly matches the one passed to AcquireMigrationLock.
- Ensure the release happens only after a successful acquire (propagate acquire errors and skip release).
- Use unique lock names per database/deployment to avoid cross-process collisions.
- If 0 persists, inspect who holds the lock (performance_schema.metadata_locks or Dolt equivalents) and stop that holder.
Example fix
// before: release called even when acquire failed
err := schema.AcquireMigrationLock(ctx, conn, name)
defer schema.ReleaseMigrationLock(conn, name)
// after: release only on success
if err := schema.AcquireMigrationLock(ctx, conn, name); err != nil {
return err
}
defer schema.ReleaseMigrationLock(conn, name) Defensive patterns
Strategy: try-catch
Try / catch
if err := schema.ReleaseMigrationLock(conn, name); err != nil {
if errors.Is(err, schema.ErrMigrationLockRelease) && strings.Contains(err.Error(), "returned 0") {
// another session owns the lock: investigate contention
}
} Prevention
- Use distinct lock names per deployment/job to avoid collisions.
- Only release after a successful acquire.
- Ensure constant lock names between acquire and release call sites.
- Investigate other holders when release reports a foreign-owned lock.
When it happens
Trigger: Calling ReleaseMigrationLock with a lockName whose lock is currently owned by another session; race where another process acquired the same-named lock after this session's lock expired or was dropped.
Common situations: Two processes using the same lockName with one having failed to acquire but proceeding to release; lock name collisions between different tools/jobs; mismatched lock names between acquire and release calls.
Related errors
- %w The Dolt database is locked.%s Try: bd doctor --fix (cle
- schema: verify fresh-bootstrap history: got %d commits, want
- %w (lock release also failed: %w)
- schema: acquire migration lock: %w: %w
- schema: acquire migration lock: %w: returned NULL
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0d75ea0cc269d6c2.
Report an issue: GitHub.