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

  1. Ensure the same *sql.Conn used for AcquireMigrationLock is passed to ReleaseMigrationLock (pin it across the migration).
  2. Treat NULL as benign if the lock ownership already ended with session teardown; log and proceed.
  3. Audit code paths that may release the lock twice or release before the migration finishes.
  4. 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

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


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