gastownhall/beads · warning · ErrMigrationLockRelease

schema: release migration lock: %w: %w

Error message

schema: release migration lock: %w: %w

What it means

ReleaseMigrationLock executes RELEASE_LOCK and the query itself failed (returned a non-nil error); the sentinel ErrMigrationLockRelease is wrapped along with the driver error. The connection is discarded (marked driver.ErrBadConn) so it is not returned to the pool in an unknown state. This signals the lock release could not be confirmed, not necessarily that the lock is still held.

Source

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

	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 {
		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. Retry release on a fresh pinned connection to the same session/database if possible.
  2. Rely on session teardown: closing the connection (or process exit) implicitly releases any GET_LOCK held by that session.
  3. Increase migrationLockCleanupTimeout or keep-alive if long migrations routinely exceed it.
  4. Check server logs/connectivity for the underlying driver error wrapped in the message.

Example fix

// before
if err := schema.ReleaseMigrationLock(conn, lockName); err != nil {
    log.Fatal(err)
}
// after: release failure usually means dead session; close and continue
if err := schema.ReleaseMigrationLock(conn, lockName); err != nil {
    conn.Close() // session teardown releases session-scoped locks
    log.Printf("release failed, connection discarded: %v", err)
}
Defensive patterns

Strategy: try-catch

Try / catch

if err := schema.ReleaseMigrationLock(conn, name); err != nil {
    if errors.Is(err, schema.ErrMigrationLockRelease) {
        conn.Close() // session teardown implicitly releases session-scoped locks
    }
}

Prevention

When it happens

Trigger: Calling ReleaseMigrationLock when the pinned session has died, the network dropped, the cleanup timeout (migrationLockCleanupTimeout) expired, or the server rejects RELEASE_LOCK for the session.

Common situations: Long migrations exceeding the cleanup timeout; MySQL server gone away after an idle period; Dolt server restart mid-migration; firewall/load-balancer closing idle connections.

Related errors


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