Ask for an answer¶
A request needs two sides: a subscription that answers it, and a caller that asks it. One side
declares a Responder instead of a Handler, and the other calls Bus.Request with a deadline.
Exactly one member of the responder's group answers, and the caller gets that reply or an error
saying why it didn't come.
Request and reply works on a backend that declares Capabilities.RequestReply. The in-memory backend
does.
Answer requests¶
messaging.SubscriptionSpec{
Name: "table-setup",
Pattern: "tables.setup",
Shape: messaging.Competing,
Group: "ingress",
Responder: func(ctx context.Context, d messaging.Delivery) (cloudevents.Event, error) {
var req SetupRequest
if err := d.Decode(&req); err != nil {
return cloudevents.Event{}, err
}
outcome := setUp(ctx, req) // "set up", or a reason it was refused
return replyEvent(outcome), nil
},
}
The event the Responder returns is the reply. A responder must be Competing: a request goes to
one member of the group, and only that member answers. The bus also needs a Settings.Source.
A refusal is a reply, not an error. "No slot left" or "that name is taken" is your application's answer, and it belongs in the reply's payload. Return an error only when the responder failed, not when its answer is "no".
When the responder does fail, by returning an error, panicking, or returning an event that cannot
be sent, the bus answers for it with a failure reply. The caller gets ErrResponderFailed straight
away, not a timeout. The error's text stays in the responder's process, where it is logged and
counted, because it can carry detail the asking process has no business seeing.
Ask¶
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
reply, err := bus.RequestValue(ctx, "tables.setup", "scoutdm.tables.setup.v1", SetupRequest{Name: "orcs"})
if err != nil {
return err
}
var outcome SetupOutcome
if err := reply.Decode(&outcome); err != nil {
return err
}
The context must carry a deadline. A request without one is refused with ErrNoDeadline before
anything is sent, because a request with no deadline, sent to a responder that has died, waits for
ever. How long an answer is worth waiting for is your decision, and the bus picks no default.
RequestValue encodes through the bus's codec and needs Settings.Source, as PublishValue does.
To set the event's ID or schema yourself, build the event and call Request.
What each error means¶
| error | what happened | retry? |
|---|---|---|
ErrNoResponders |
nothing is subscribed to the subject, so nothing received it | safe: nothing acted on it |
ErrNoReply |
the context ended first; errors.Is tells context.DeadlineExceeded from context.Canceled |
only if the responder's work is safe to repeat |
ErrResponderFailed |
the responder erred or panicked, and its process has the detail | only if the responder's work is safe to repeat |
ErrStopped |
Stop began while the request was in flight, and cancelled it |
after the bus is running again |
| the transport's error | the backend refused the request, for example a full responder under Fail |
depends on the error |
Refused before anything is sent, and not counted: ErrNoDeadline; ErrNoReply for a context that
had already ended; ErrNotReady when the bus has no live activation; ErrNotSupported on a backend
that does not answer requests; and ErrReservedAttribute for an event carrying messagingfailed,
the attribute that marks a failure reply.
ErrNoResponders means nothing is subscribed, not that no responder is. If only observers or
plain handlers subscribe to the subject, the request is delivered, nobody answers, and you get
ErrNoReply at the deadline.
The bus never retries for you. A retry inside the bus would hide the count you need, and is only safe when the responder is idempotent, which only you know.
Count requests by conversation¶
messaging.Settings{
RequestNames: []string{"table-setup", "audit"},
}
reply, err := bus.Request(ctx, "tables.setup", event, messaging.WithRequestName("table-setup"))
bus.RequestStatsFor("table-setup") is that conversation's row, RequestStatsFor("") is requests
sent without a name, and bus.RequestStats() is the total. Names are declared up front, like
subscription names, so the number of rows is fixed by your code and not by your traffic. A name you
didn't declare is counted with the unnamed requests, and logged once; RequestStatsFor with such a
name reads as zero.
Once nothing is in flight, Sent equals Answered + NoResponders + NoReply + Failed. Unclaimed is
replies that arrived after their request had finished: a responder slower than its caller's deadline,
or a second group answering the same subject. It is a bus-wide count and appears only in the total.
On the answering side, ConsumerStats.Replies says what became of each reply: Replied, Refused
by the backend, or Unaddressed.
Moving from publish to request¶
A responder still runs for a plain publish on its subject. The reply has nowhere to go, so it is
discarded and counted Unaddressed. That means you can turn a subscription into a responder while
publishers are still publishing to it: their work still gets done, and you can move them to
Request one at a time.
Rules the bus enforces¶
Each of these is refused when the bus is built, and again by a late Subscribe:
- a subscription with both a
Handlerand aResponder; - a responder that is
Fanout, orAtLeastOnce: a redelivered request would answer nobody; - two responders in different groups whose patterns can select the same subject, because both would answer it;
- a
Handlerin a responder's group, which would take requests it cannot answer; - a responder on a backend without
Capabilities.RequestReply, or on a bus withoutSource.
The bus can't see other processes' subscriptions. Run one responder group per subject across your
deployment. A second group's reply is counted Unclaimed rather than delivered.