gastownhall/beads · warning · ErrMigrationLockRelease
schema: release migration lock: %w: returned NULL
Error message
schema: release migration lock: %w: returned NULL
What it means
RELEASE_LOCK returned NULL, which in MySQL semantics means the current session does not own the named lock (or the lock does not exist). The library wraps ErrMigrationLockRelease and discards the connection since ownership cannot be established. It indicates the acquire/release pairing was broken.
Source
Thrown at internal/storage/schema/lock.go:376
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)
}
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
- Ensure the same *sql.Conn used for AcquireMigrationLock is passed to ReleaseMigrationLock (pin it across the migration).
- Treat NULL as benign if the lock ownership already ended with session teardown; log and proceed.
- Audit code paths that may release the lock twice or release before the migration finishes.
- Add logging/correlation of conn identity between acquire and release to spot pool mixing.
Example fix
// before: conn fetched fresh from pool for release releaseConn, _ := db.Conn(ctx) schema.ReleaseMigrationLock(releaseConn, lockName) // after: reuse the pinned acquisition connection schema.ReleaseMigrationLock(pinnedConn, lockName)
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 NULL") {
// we no longer owned the lock; log and continue
}
} Prevention
- Pair each Acquire with exactly one Release on the same conn.
- Never fetch a fresh conn from the pool for release.
- Skip release if acquire failed.
- Track lock ownership in code (bool) before releasing.
When it happens
Trigger: Calling ReleaseMigrationLock on a connection different from the one that called AcquireMigrationLock; the acquiring session already timed out or was reset; the lock was already released on this session.
Common situations: Connection-pool mixing — re-acquiring a *sql.Conn from the pool instead of reusing the pinned conn; a prior release attempt that half-succeeded; process reconnect after a transient network error replaced the session.
Related errors
- %w The Dolt database is locked.%s Try: bd doctor --fix (cle
- %w (lock release also failed: %w)
- schema: acquire migration lock: %w: %w
- schema: acquire migration lock: %w: returned NULL
- schema: acquire migration lock: %w: timeout
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d813ce10d8dec9e6.
Report an issue: GitHub.