{"record":{"id":"63d5c58a736cd227","repo":"sipeed/picoclaw","slug":"events-bus-is-closed","errorCode":null,"errorMessage":"events: bus is closed","messagePattern":"events: bus is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/events/subscription.go","lineNumber":17,"sourceCode":"package events\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"runtime/debug\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\nconst defaultSubscriberBuffer = 16\n\nvar (\n\t// ErrBusClosed is returned when subscribing to a closed event bus.\n\tErrBusClosed = errors.New(\"events: bus is closed\")\n\t// ErrNilHandler is returned when subscribing without a handler.\n\tErrNilHandler = errors.New(\"events: handler is nil\")\n)\n\n// Handler processes a runtime event delivered to a subscription.\ntype Handler func(context.Context, Event) error\n\n// SubscribeOptions controls how a subscription receives events.\ntype SubscribeOptions struct {\n\tName         string\n\tBuffer       int\n\tPriority     int\n\tConcurrency  ConcurrencyKind\n\tBackpressure BackpressurePolicy\n\t// Timeout bounds how long the subscription worker waits for one handler call.\n\t// Handlers should still honor ctx cancellation; timed-out calls keep running\n\t// until their handler returns.\n\tTimeout     time.Duration","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/events/subscription.go#L1-L35","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Treat it as a stop signal: check errors.Is(err, events.ErrBusClosed) in subscribe-retry loops and exit the loop instead of retrying","Create a fresh EventBus for the next lifecycle instead of reusing the closed one","Fix shutdown ordering: stop/quiesce subscriber components before calling bus.Close()","If it appears on startup, the bus pointer is nil — initialize it before passing it to components"],"exampleFix":"// before\nfor {\n    sub, err := ch.Subscribe(ctx, opts, h)\n    if err != nil { continue } // spins forever after shutdown\n}\n\n// after\nsub, err := ch.Subscribe(ctx, opts, h)\nif err != nil {\n    if errors.Is(err, events.ErrBusClosed) { return } // expected during shutdown\n    return err\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isBusClosed(err error) bool {\n    return errors.Is(err, events.ErrBusClosed)\n}","tryCatchPattern":"sub, err := ch.Subscribe(ctx, opts, handler)\nif err != nil {\n    if errors.Is(err, events.ErrBusClosed) {\n        return nil // expected during shutdown: stop subscribing, unwind\n    }\n    return err // real failure: log and apply backoff/retry\n}","preventionTips":["Never reuse a bus after Close(); create a new EventBus per lifecycle","Close subscriber components before closing the bus during shutdown","Track a 'shutting down' flag so re-subscribe loops stop before touching the closed bus"],"tags":["events","lifecycle","concurrency","shutdown"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}