0059 — Sink durability: the acknowledgement says how durable, and the start-up refuses less¶
Status: accepted; supersedes 0054 and extends 0046 Date: 2026-10-02
Supersedes 0054. Extends 0046: its sink contract and adapter model stand, and this adds what an acknowledgement carries and which transports exist.
Context¶
0054 made the receiver's acknowledgement mean "durable", and defined durable per mode: a replicated publish in stream mode, an object in the bucket in direct mode. That holds while there are two shapes. It does not hold once a record can reach the archive through a queue, a function invocation, a process-local spool or, as a last resort, a log line: the word "durable" now covers things that differ by orders of magnitude in what they survive, and an application cannot tell which one it got.
0054 also let a full emitter queue drop its oldest records and count them. For a component whose point is a complete trail, a drop is a decision made by an overflow rather than by a person, and the caller cannot tell it happened.
Decision¶
The acknowledgement carries a durability, one of:
| durability | means | survives |
|---|---|---|
Archived |
the record is in the archive's bucket | everything the bucket survives |
Queued |
a durable, replicated queue holds it and will deliver it to the archive | the loss of a pod or a node, not of the queue |
Logged |
the process wrote it to its log | nothing but the log pipeline |
Durabilities are ordered, Archived over Queued over Logged. The
Result of SinkService.Write names the durability for the whole batch.
Every hop is a Sink, and a Sink wraps the next. A chain is built from configuration: a spool in front of a queue publisher, a queue consumer in front of the in-process writer. A hop returns the durability of the last hop that took the batch, and a wrapper never reports more than its successor did.
require: is a start-up guard. The configuration names the weakest
durability a sink chain may give (require: archived, queued or
logged). The process computes the best the chain it was configured with can
ever give and refuses to start if that is weaker. A write whose
acknowledgement is weaker than required at run time is a failure, not a
success with a caveat.
A full spool fails the write. A bounded queue in front of a sink that
cannot keep up refuses the next record with an error the caller sees,
instead of dropping the oldest. block waits for an acknowledgement that
meets require; async returns once the spool has the record, and a spool
that cannot take it returns the error to the caller. Dropping is no longer a
behaviour; OnDropped stays as the hook for the records a process loses
because it died.
Transports, by platform:
| platform | transports |
|---|---|
| Kubernetes | nats (JetStream, Queued), http (Connect to the receiver) |
| AWS | sqs (Queued), lambda (a direct invocation of the writer function) |
| everywhere | s3 (in process: the writer's own put, Archived), log (Logged) |
Each transport is a Sink and a matching consumer; none touches the emitter
or the writer. log exists for development and for the deployment that has
accepted that a log pipeline is its record, and require: is what keeps it
from being the accident.
Consequences¶
- What an application was promised is a value, not a convention. The
same call returns
Archivedin one deployment andQueuedin another, and a deployment that needs the first says so inrequire:. - A misconfigured chain fails at start-up, not on the first privileged
action:
require: archivedover a queue-only chain never starts. - Backpressure is visible. A full spool is an error at the call site and a metric, where before it was a counter nobody was forced to read.
- The delivery modes
blockandasyncof 0054 stay as the catalogue's per-action choice of whether the caller waits; what changes is what they wait for and what a full queue does.
Alternatives considered¶
- Keep "durable" as one word. It is what 0054 chose, and it is what stopped working once a queue and a function platform existed beside the bucket.
- Make
Archivedthe only acceptable answer. Simple, and wrong for a deployment where a replicated queue is the real boundary and the bucket is one hop behind it; it would send those deployments to invent their own weaker acknowledgement. - Keep dropping the oldest when a queue fills. Cheaper for the caller; it turns a capacity problem into silent loss, which is the one failure this component exists to make loud.