What this module does not do¶
Each of these is a decision with a name, not a gap waiting to be filled. Several are easy to add, which is exactly why they are written down.
No persistence, no replay, no durability, by default¶
The bus itself keeps nothing: no queue of its own, no replay, no state to operate. That is a refusal rather than an omission. A bus that gains state has gained a component somebody has to operate: backup, migration, corruption, capacity, a second thing to be down.
What a backend can do beyond that, it declares. SurvivesRestart says whether an unacknowledged
message outlives the process; Replays says whether a new durable identity can begin at the oldest
message the backend still retains. The in-memory backend declares neither: nothing survives its
restart, and a subscriber that starts late has missed what came before. A NATS backend on JetStream
declares both, and a subscription opts in by naming a DurableName and a delivery Mode; a queue
built on SQS, RabbitMQ or AMQP declares SurvivesRestart without Replays, because a queue delivers
what it holds rather than a log a new reader can rewind.
If you want none of that, a subscription's default, AtMostOnce with no DurableName, is exactly
what this section describes and costs nothing to ask for.
No delivery guarantee beyond what a subscription asks for and a backend declares¶
The default, AtMostOnce, promises one attempt and that a failure is counted, not that delivery
happened. AtLeastOnce is the stronger mode this module offers: a subscription names a
DurableName, the backend retries until a handler settles it with Ack or Nack, and a mode the
backend has not declared is refused at construction rather than silently downgraded. See
settle a delivery for what a handler owes under each mode, and
the backend contract for what a backend has to declare to offer it.
What no mode promises, ever, is exactly-once: a redelivered message is a duplicate a handler has to
tolerate, under both SlowHandler policies, which is why an at-least-once handler must be
idempotent or safe under concurrent execution of the same message.
No ordering across subscribers¶
Each subscriber sees its own messages in order. Nothing coordinates two subscribers with each other.
And per-subscriber ordering holds only at Concurrency: 1. A subscription with more than one
reader surrenders it, deliberately and at your choosing at wiring time: Concurrency: 1 keeps
ordering, anything above it does not, and there is no runtime scaling that could take it away
without you asking. See
scale a subscription with concurrency.
No universal broker abstraction¶
Spanning SQS, Kafka and JMS is explicitly not the goal. The intersection of every broker is "publish bytes to a name", and an abstraction built to that is not worth having.
What is claimed instead is considerably less: two delivery shapes that were measured portable, and
everything beyond them declared as a capability and discovered rather than assumed. Four provider
modules hold to the same conformance suite this module ships:
messaging-nats,
messaging-sqs,
messaging-rabbitmq and
messaging-amqp. That is what tests an
abstraction shaped by one implementation: a queue with no wildcard subscription and no fan-out
without a second service found gaps a channel-backed backend could not, because in-process channels
can do everything core NATS can minus the network.
No real-time media streams¶
Audio and video frames do not belong here, and the reasons are three decided properties rather than a performance worry:
- the unit is a CloudEvent, so a frame is wrapped per frame, per speaker, per source — and on a
fan-out subject the payload is copied again per subscriber, in
CopyEvent, so two subscribers cannot see each other's mutations; - a shed is silent to the publisher under the default at-most-once mode, and media dropped at a
full queue cannot be re-requested the way
AtLeastOnceretries a message a handler declined; - per-subscriber ordering survives only at
Concurrency: 1, so reconstructing a stream from more than one reader means reordering it yourself, which is exactly the cost this module refuses to hide behind a guarantee it cannot keep.
Those are the same three properties that make the bus good at what it is for. A component wanting the opposite of all three wants a different component.
Two of those three used to be stated more strongly than they deserved, and the corrections are worth keeping rather than quietly folding in.
The copy is ours, not the envelope's. This page used to say a CloudEvent forces a copy per frame.
It does not — cloudevents.Marshal aliases Data deliberately, because a codec on a message hot
path should not double every payload. The copy is real, but it is CopyEvent and it happens once per
subscriber, which makes this a weaker objection for a single competing consumer than it reads.
Silent means silent to the publisher. A shed is not unrecorded: it is counted and attributed to the subscription that fell behind, which is this module's one actual guarantee. For a consumer whose requirement is counted, not prevented, that distinction is the whole answer.
The refusal stands on the third property, and on the second read correctly. It should not have stood on the first.
Media is out; a reference to stored media is not. A pointer to a segment already written to object storage, with a manifest, is an ordinary event: serialisable, low rate, and recoverable rather than merely tolerable when one is shed, because the segment is durable and the reference can be rebuilt. What is refused is putting the bytes on the bus, not knowing about them.
No circuit breaker, no health-based routing¶
Nothing here reroutes traffic away from a struggling dependency, a degraded competing-consumer
member, or a handler that keeps declining. RoutesAroundDegraded names the middle one as a
capability precisely because it is almost always false: "queue group" suggests load balancing, and
NATS distributes rather than balances, so a wedged member keeps receiving its share, fills, and
sheds while a healthy sibling idles.
Under AtLeastOnce, MaxDeliver bounds how many times a message is retried, and a subscription's
Backoff spaces the retries out, so a transient failure does not spend the whole budget in
milliseconds. That is a delay on one message, not a judgement about the dependency it was waiting
for: nothing here notices that every message is failing for the same reason and stops sending them.
See settle a delivery.
What this module does offer instead is supervision at the subscription boundary: a subscription
whose readers all fail is quarantined and, under its RestartPolicy, restarted on a backoff of its
own. That is a property of the subscription surviving, not of one message's delivery being retried
intelligently, and the two are easy to conflate and answer different questions.
One answer per request, and nothing durable about it¶
Request and reply is one request, one answer, within a deadline the caller sets. What it does not do:
- More than one answer.
Bus.Requestreturns the first reply. Scatter-gather and streaming replies are not offered. A second responder group answering the same subject has its reply discarded and countedUnclaimed. The bus refuses that layout within one process, but it cannot see other processes, so one responder group per subject is a rule you keep across a deployment. - Survive a restart. A responder is
AtMostOnce, because a request redelivered after its caller's deadline would answer nobody. When a responder's consumer restarts, the requests queued behind it are discarded, and each caller getsErrNoReplyat its deadline: countedNoReplyon the asking side andLossRetiredQueuedon the answering one. - Choose a deadline, or retry. A request whose context has no deadline is refused with
ErrNoDeadline, because the bus picks no timeout for you. Nor does it retry:ErrNoRespondersis safe to retry,ErrNoReplyandErrResponderFailedonly when the responder's work is safe to repeat, and only your application knows that. - Carry a failure's detail back. A responder that errs or panics answers with
ErrResponderFailedand nothing more. The detail stays in the responder's process, where it is logged and counted, because it can carry what the asking process has no business seeing. - Run on every backend. Only a backend declaring
Capabilities.RequestReplyanswers requests: the in-memory backend andmessaging-nats' core NATS backend do. Its JetStream backend, SQS, RabbitMQ and AMQP do not, because a durable request is a different contract and nobody has needed request and reply over a queue.Bus.Requeston one of them returnsErrNotSupported, and aResponderis refused when the bus is built.
No configuration library¶
This module takes typed settings. The calling application maps its configuration onto them, and secrets arrive as a credential source rather than a string. A toolkit module that reads configuration decides for its consumer where configuration comes from.