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.
Make a declined message wait¶
By default a Nack brings the message back at once. A handler that nacks a transient failure, such
as a dependency that is down for a second, would then use up a MaxDeliver of five in milliseconds.
Give the subscription a Backoff, and each decline waits before the next attempt:
messaging.SubscriptionSpec{
Name: "orders", Pattern: "orders.>", Shape: messaging.Fanout,
Mode: messaging.AtLeastOnce, MaxDeliver: 5, DurableName: "orders",
Backoff: messaging.Backoff{Initial: time.Second, Max: time.Minute, Multiplier: 2},
MaxNackDelay: 10 * time.Minute,
Handler: handle,
}
Nack now waits one second after the first attempt, two after the second, then four, capped at a
minute. A handler that returns without settling has declined too, so a plain return err waits
the same schedule; d.NackAfter(0) is how to ask for an immediate retry instead. A handler that has been told when to come back, by a Retry-After for example, says so for
that one decline:
NackAfter is allowed up to MaxNackDelay, or the schedule's Max when that is unset. On a
subscription with neither, or for a delay that is negative or above that ceiling, it returns an error
and records nothing, so the handler can still Nack. A delay is never quietly turned into an
immediate retry.
What a delay promises: the message is not offered again before the delay has passed. It may come
later, if the queue is full or the consumer is not reading. The declined attempt is spent, so it
still counts towards MaxDeliver.
A delay needs a backend that can hold the message back, Capabilities.DelaysRedelivery. On one that
cannot, the subscription is refused when the bus is built, as it is for a delay longer than the
backend's MaxRedeliveryDelay. Neither is allowed under AtMostOnce, where nothing comes back.
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
st.DelayShortened // declines redelivered sooner than their delay asked, because the backend could not hold them that long
Released and LeaseExpired are attempt-grain: one message can be released, or reach the bound,
more than once, so neither is a message count. DelayShortened is zero except on a backend with a
limit on how long it can hold a message: SQS cannot hide one past twelve hours from its first
receive. Exhausted and Stranded are message-grain. On a
backend declaring Capabilities.RestartCanSpendUnseenAttempt, Exhausted can undercount: a restart
can spend a message's last attempt where no process saw it, and then nothing counts it.
messaging-nats' JetStream backend declares it; see the backend reference. 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.
ForeignConflicts, under any mode, is attempts that arrived with a transport header contradicting
the event's envelope, such as a broker's correlation ID that differs from the one in a MassTransit
envelope. It is attempt-grain and outside the identity too. The event is still the envelope's; the
other value is in d.Foreign().Headers. Binary and structured mode never raise it.
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 or OnSettleWith 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 — and a fifth, a declined message waiting out its delay, on a backend declaring
DelaysRedelivery. 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, or
messaging.SettleWith to put a delay on a decline. One path, not two.