feat: web, websocket 어댑터 추가 구현
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# ADR-WEB-ADV-001: Streaming is live delivery, and the web module stores no history
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-25
|
||||
- Scope: `adapter:inbound:web` — `advanced.stream.**`
|
||||
|
||||
## Context
|
||||
|
||||
Advanced Tasks 6–13 add SSE, NDJSON and JSON text sequences, with a `Last-Event-ID` resume path.
|
||||
|
||||
One fact drives every decision here: **after the first byte, the HTTP status is 200 and cannot
|
||||
change.** A stream that ends because a dependency failed and one that ends because it finished are
|
||||
identical at the transport layer — both are a closed connection after a 200. So is a stream that was
|
||||
cut off mid-flight.
|
||||
|
||||
The second fact is that a resume path invites the web module to remember things. It must not: the
|
||||
messaging platform already owns durable event history, and a second copy would have its own
|
||||
retention, its own eviction and its own opinion about ordering.
|
||||
|
||||
## Decision
|
||||
|
||||
**Three outcomes, expressed in the stream rather than in the status.** `WebStreamEnvelope` is sealed
|
||||
over `Item`, `Failure` and `Complete`. A client that sees neither terminal envelope has been cut off,
|
||||
and that third case is recorded as `ABRUPT_CLOSE` rather than counted as a completion — which is
|
||||
where a rising rate of mid-stream failures would otherwise hide.
|
||||
|
||||
**Nothing writes a problem document onto a committed response.** `WebStreamTerminationMapper`
|
||||
branches on whether any byte has been written. Before commit, an RFC 9457 problem with a real
|
||||
status; after, a terminal record. Attempting both produces a body that is half stream and half JSON,
|
||||
which no client parses and every proxy caches as a success.
|
||||
|
||||
**Positions are monotonic, and it is enforced.** `WebStreamEvidence.recordDelivered` refuses a
|
||||
repeated or regressing position. A client deduplicating on position would silently drop the second
|
||||
item.
|
||||
|
||||
**A slow consumer is disconnected, not buffered.** `WebStreamPolicy.maxBufferedItems` is a hard
|
||||
bound. Backpressure protects the reactive pipeline; it does not protect the server's heap from a
|
||||
consumer that reads slowly for an hour.
|
||||
|
||||
**Every stream is in a registry, and shutdown drains it.** A node with a hundred open streams and no
|
||||
other traffic looks idle by request rate. `WebStreamDrainCoordinator` stops accepting first, asks
|
||||
clients to reconnect, and only then forces the remainder — because a client whose socket is cut
|
||||
retries immediately, and if every socket is cut at once, every client retries at once.
|
||||
|
||||
**The web module stores no durable history.** `WebStreamReplaySource` is an interface this module
|
||||
implements nowhere. An expired cursor raises `ReplayCursorExpiredException` rather than resuming from
|
||||
the oldest retained position, because that delivers a stream with a hole the client cannot see.
|
||||
|
||||
**The replay-to-live seam is watched.** `GapAndDuplicateGuard` detects both directions. Neither is
|
||||
visible in either half on its own.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Clients must handle three outcomes. A client that treats a closed connection as completion will be
|
||||
wrong, and no server change can fix that for it.
|
||||
- An expired `Last-Event-ID` costs the client a full re-read. That is the honest answer.
|
||||
- JSON-seq is preferred over NDJSON where truncation matters: its separator comes first, so a parser
|
||||
resynchronises at the next record. NDJSON's delimiter is the thing that gets truncated away.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Emit a problem document when a stream fails after commit.** Rejected: the body becomes
|
||||
unparseable and the 200 is cached.
|
||||
- **Resume from the oldest retained position when the cursor expires.** Rejected: positions are
|
||||
contiguous from where the replay started, so nothing in the data says events are missing.
|
||||
- **Store replay history in the web module.** Rejected: a second source of truth that drifts
|
||||
invisibly.
|
||||
- **Unbounded buffering for slow consumers.** Rejected: it moves the client's slowness into the
|
||||
server's heap.
|
||||
Reference in New Issue
Block a user