sipeed/picoclaw · warning

events: bus is closed

Error message

events: bus is closed

What it means

Returned by the events.EventBus Subscribe family (bus.go also returns it for a nil *EventBus, and Publish paths surface the analogous condition) when subscribing to a bus that is nil or already closed via Close(). During gateway shutdown this is an expected signal — internal agent code (agent_outbound.go) explicitly treats errors.Is(err, bus.ErrBusClosed) as non-fatal.

Source

Thrown at pkg/events/subscription.go:17

package events

import (
	"context"
	"errors"
	"log"
	"runtime/debug"
	"sync"
	"sync/atomic"
	"time"
)

const defaultSubscriberBuffer = 16

var (
	// ErrBusClosed is returned when subscribing to a closed event bus.
	ErrBusClosed = errors.New("events: bus is closed")
	// ErrNilHandler is returned when subscribing without a handler.
	ErrNilHandler = errors.New("events: handler is nil")
)

// Handler processes a runtime event delivered to a subscription.
type Handler func(context.Context, Event) error

// SubscribeOptions controls how a subscription receives events.
type SubscribeOptions struct {
	Name         string
	Buffer       int
	Priority     int
	Concurrency  ConcurrencyKind
	Backpressure BackpressurePolicy
	// Timeout bounds how long the subscription worker waits for one handler call.
	// Handlers should still honor ctx cancellation; timed-out calls keep running
	// until their handler returns.
	Timeout     time.Duration

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Treat it as a stop signal: check errors.Is(err, events.ErrBusClosed) in subscribe-retry loops and exit the loop instead of retrying
  2. Create a fresh EventBus for the next lifecycle instead of reusing the closed one
  3. Fix shutdown ordering: stop/quiesce subscriber components before calling bus.Close()
  4. If it appears on startup, the bus pointer is nil — initialize it before passing it to components

Example fix

// before
for {
    sub, err := ch.Subscribe(ctx, opts, h)
    if err != nil { continue } // spins forever after shutdown
}

// after
sub, err := ch.Subscribe(ctx, opts, h)
if err != nil {
    if errors.Is(err, events.ErrBusClosed) { return } // expected during shutdown
    return err
}
Defensive patterns

Strategy: try-catch

Type guard

func isBusClosed(err error) bool {
    return errors.Is(err, events.ErrBusClosed)
}

Try / catch

sub, err := ch.Subscribe(ctx, opts, handler)
if err != nil {
    if errors.Is(err, events.ErrBusClosed) {
        return nil // expected during shutdown: stop subscribing, unwind
    }
    return err // real failure: log and apply backoff/retry
}

Prevention

When it happens

Trigger: Calling Subscribe/SubscribeOnce on a *EventBus that is nil, or after bus.Close() has completed; a re-subscribe loop racing shutdown ordering (bus closed while a component tries to reconnect).

Common situations: Shutdown races where a subscriber goroutine re-subscribes after the gateway closed the bus; storing a bus pointer that was never initialized (nil receiver path in bus.go:173); reusing a bus instance across test runs without recreating it.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/63d5c58a736cd227. Report an issue: GitHub.