Skip to content

Subscribe at runtime

err := bus.Subscribe(ctx, messaging.SubscriptionSpec{
    Name:    "tenant-01JQ",
    Pattern: "tenant.01JQ.>",
    Shape:   messaging.Fanout,
    Handler: handleTenantEvent,
})

The handler is part of the spec, as it is at construction: there is no separate step to bind one, because a subscription without a handler is unrepresentable here on purpose. A subscription added this way is validated exactly as a wiring-time one, and supervised identically. That is the point of the type underneath: a controls.Controller declines to supervise a late registration and says so, and the Supervisor under this bus takes a child before or after start.

Subscribe returns once the subscription's first subscribe has succeeded, so when it returns nil the subscription is live. If the backend refuses the first subscribe, the error says so and nothing is left behind; a restart policy governs failures only after a first success, as at boot.

When you need it

Two cases, and the second is the one people miss.

Sets not knowable at wiring. A subscription per tenant, per channel, per guild.

Taking a bigger share of a burst. This is the subtle one. A competing group routes by subscription, not by reader: the in-memory backend picks the least-loaded source for each message, and a source is one per subscription whatever its Concurrency, so a member with four readers still receives one member's share of the group.

So the two mechanisms do different jobs. Concurrency processes the share you are given, faster, and prevents shedding on that share. More subscriptions take a bigger share, and change distribution. For a burst one instance must absorb more of, only the second works. See scale a subscription with concurrency for the first.

What it costs

A late subscriber has missed what came before, and nothing counts what it missed. That is at-most-once pub/sub rather than a new hazard: a subscriber sees traffic from the moment it attaches. Under AtLeastOnce on a backend that replays, StartAt says otherwise.

Ready() still refers to the wiring-time set, so a consumer gating on it gets exactly the guarantee it did before. A subscription added before Start is simply built with the rest.

The refusals

if errors.Is(err, messaging.ErrStopped) {
    // shutdown has begun, or the bus has stopped: nothing will supervise it
}

if errors.Is(err, messaging.ErrNotReady) {
    // the bus is starting; wait on Ready() and try again
}

Once shutdown has begun, Subscribe refuses. Accepting a subscription that will never be supervised sends the caller away believing it has one, which is worse than an error it can see. And a setting the backend does not offer is refused here rather than degraded, as it is at construction, because a caller adding one subscription can act on an error where a bus under construction could not.