Settle a delivery¶
A handler tells the bus what it thinks of a delivery by calling Ack or Nack on it. This
page is how to do that, what the bus does with it today, and the two rules that hold whichever
mode a subscription is in.
Say what happened¶
Handler: func(ctx context.Context, d messaging.Delivery) error {
var order Order
if err := d.Decode(&order); err != nil {
return d.Nack() // malformed: decline it
}
if err := process(ctx, order); err != nil {
return err // an error is an outcome, and it is separate from settlement
}
return d.Ack()
},
Ack accepts the delivery. Nack declines it. Returning without calling either is silence,
and the bus records it as such rather than reading it as consent.
What the bus does with it¶
That depends on the subscription's Mode.
Under AtMostOnce — the default — nothing is acted on: a Nack does not bring the message
back. Every settlement is still recorded, in the Settlement counters on ConsumerStats, and a
handler that settles twice is refused.
Under AtLeastOnce a Nack, or silence, brings the message back, up to MaxDeliver attempts.
The subscription also names, in DurableName, the identity that outlives the process: it is required
in this mode on every backend, so the spec you test against memory is the spec you run against a
broker.
Each redelivery arrives with a higher Attempt. A handler that keeps declining exhausts the budget
and the message is counted Exhausted — real data loss, reported at message grain. The same handler
is correct in both modes; only what the bus does with its answer differs.
Redelivery is immediate, and there is no backoff yet. A handler that nacks a transient failure —
a dependency that is down for a second — burns a MaxDeliver of five in milliseconds, which makes
the budget useless for the case it exists for. Until backoff lands, a handler that expects transient
failures should absorb the wait itself before it nacks, or size MaxDeliver for the retries it
actually wants.
If your handler is slow¶
A broker cannot tell slow from dead. After AckWait it assumes dead and hands the message on, and
the overlap spike
measured one message running in three handlers at once when that guess was wrong. SlowHandler is
your choice of what happens instead:
ExtendLease, the default, keeps the message yours for up toMaxProcessingTime(five minutes unless you say otherwise). Your handler must be idempotent, because a partition or a failover can still hand the message on and nothing can prevent that.Redeliverlets the message go while you may still be running. Your handler must be safe under concurrent execution of the same message, which usually means a lock or a unique constraint, andAckWaitbecomes a correctness setting: shorter than your slowest run, and duplicate work is routine.
The default is the safe one because Redeliver's failure is silent duplicate work under load. The
in-memory backend never hands a message on while a handler runs, so neither setting does anything
there; they are for the backends that do.
Two rules that hold in every mode¶
Exactly one settlement per delivery. The first call wins; a second returns
ErrAlreadySettled. This holds even if you copy the Delivery — into a struct, a closure, another
goroutine — because the settlement state is shared behind the value. A handler that calls both Ack
and Nack has a bug, and the bus can see it.
The returned error and the settlement are separate. An error is counted and logged and does
not affect settlement; the settlement is what decides redelivery once a mode acts on it. So
Ack-then-return-error and Nack-then-return-nil are both legal and both mean exactly what they
say. If you are used to a system where returning an error is the nack, this is the difference to
hold onto.
Reading it back¶
st, _ := bus.Stats("orders")
st.Settlement.Acked // handler accepted
st.Settlement.Nacked // handler declined
st.Settlement.Silent // handler said nothing
st.Settlement.Refused // the backend could not perform a settlement — a different axis
The first three partition Delivered. Refused is orthogonal: a nack the backend could not act on
moves Nacked and Refused.
Under AtLeastOnce four more counters say where attempts and messages went:
st.Released // attempts given up before a handler returned, owed back by the backend
st.Exhausted // messages that ran out of attempts — lost, at message grain
st.Stranded // messages declined whose backend could not be asked to try again; zero where the broker will
st.LeaseExpired // attempts handed on at MaxProcessingTime while the handler may still have been running
Released and LeaseExpired are attempt-grain: one message can be released, or reach the bound,
more than once, so neither is a message count. Exhausted and Stranded are message-grain. All four
sit beside the attempt identity, never inside it — the identity at rest is
Offered == Delivered + Released + Lost. LeaseExpired in particular changes nothing in the
identity: the attempt it counts is still running and is Delivered when its handler returns. It says
only that a duplicate is now possible, which is exactly what you need to see when ExtendLease ran
out of road.
If you write a backend¶
Construct deliveries through NewDelivery with a DeliveryOptions, never as a struct literal. The
settlement state is unexported precisely so that copies cannot disagree, and NewDelivery is where
a backend hands the bus its Attempt count and its OnSettle callback. See the
backend reference.
The conformance suite holds an AtLeastOnce backend to four rows — redelivery with an increasing
attempt, MaxDeliver and Exhausted, per-message acknowledgement, and a refused settlement after
retirement. It drives the backend at the seam with no bus above it, so it completes each exchange
with messaging.Settle, the same function the bus calls after a handler returns. One path, not two.