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, no backoff on redelivery¶
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, but redelivery is
immediate, with no backoff yet: a handler that nacks a transient failure burns its whole budget in
milliseconds, which is useless for the failure it exists to survive. Until backoff lands, a handler
that expects a dependency to recover should absorb the wait itself before it nacks, or size
MaxDeliver for the retries it actually wants. 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.
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.