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

  1. Verify the release lockName exactly matches the one passed to AcquireMigrationLock.
  2. Ensure the release happens only after a successful acquire (propagate acquire errors and skip release).
  3. Use unique lock names per database/deployment to avoid cross-process collisions.
  4. 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

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


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