docs: correct WS framing claims from spike; add implementation plan
Spike against alkcall source resolved ADR-067 assumptions: - write_chunk issues header+payload as separate write_alls; channel 0's write_frame issues prefix+body separately — a logical write can surface as multiple chunks, so the WS adapter must parse outgoing chunk boundaries (byte-stream treatment both directions), not assume write-per-chunk or message-per-chunk - MAX_CHUNK_LEN is 16 MiB; the WS path needs a practical message cap with oversized chunks split across messages - install_channel_zero + run_loop_single_stream confirmed as the exact server-side seam; EOF/teardown invariants already specified by alkcall (REQ-CH-01/02) Corrections applied to websocket.md, ADR-067, OQ-01. docs/plans/implementation.md: scoped plan guiding task decomposition — spike findings, 4-phase build order, OQ dispositions, task conventions.
This commit is contained in:
@@ -46,18 +46,23 @@ in the alknet design). The path is an axum route on the `HttpAdapter`
|
||||
router, subject to the same reserved-path collision rule as any
|
||||
default-surface route ([ADR-046](046-assembly-layer-custom-http-routes.md)).
|
||||
|
||||
### Framing: the WS message boundary carries chunks, not envelopes
|
||||
### Framing: the chunk header is the boundary, not the WS message
|
||||
|
||||
The alknet design's "one `EventEnvelope` = one binary WS message, no
|
||||
length prefix" framing is **superseded**. The WS message boundary now
|
||||
carries channels chunks; the call protocol's envelopes ride inside
|
||||
channel 0 as length-prefixed JSON (alkcall ADR-014 frame format), the
|
||||
same as any other in-line channels transport.
|
||||
length prefix" framing is **superseded**. The WS binary message stream
|
||||
is treated as a byte stream; the 8-byte chunk header is the only
|
||||
framing. Call-protocol envelopes ride inside channel 0 as
|
||||
length-prefixed JSON (alkcall ADR-014 frame format), the same as any
|
||||
other in-line channels transport. A chunk may span WS messages and a
|
||||
WS message may carry chunk fragments (in practice the write path
|
||||
usually produces one chunk per message, but nothing may depend on
|
||||
it — channel 0's frame writer issues two writes, prefix then body,
|
||||
which can surface as two chunks).
|
||||
|
||||
Layering on the wire, for a call frame over WS:
|
||||
|
||||
```
|
||||
WS binary message
|
||||
WS binary message(s) — byte stream
|
||||
└── chunk header [channel_id: u32 BE][length: u32 BE] (8 bytes)
|
||||
└── payload = frame [len: u32 BE][EventEnvelope JSON] (channel 0)
|
||||
```
|
||||
|
||||
@@ -22,15 +22,22 @@ with their resolutions; new alkhttp OQs start at OQ-01.
|
||||
needs nailing down before implementation:
|
||||
(a) inbound buffer bound (bounded channel between the WS read task
|
||||
and the `AsyncRead` half — what bound, what policy on overflow);
|
||||
(b) write-side chunk completeness (the adapter assumes the mux emits
|
||||
each chunk as one contiguous `write_all` — verify against alkcall's
|
||||
`MuxRunner` and codify, or add an internal chunking layer);
|
||||
(b) write-side chunk boundary parsing (verified against alkcall
|
||||
source: the mux emits one mpsc payload per chunk, but a logical
|
||||
write above the mux — e.g. channel 0's `write_frame`, which issues
|
||||
prefix and body as separate `write_all`s — can surface as multiple
|
||||
chunks; the adapter must parse outgoing chunk headers rather than
|
||||
assume write-per-chunk; also confirm the WS-message cap policy for
|
||||
chunks up to `MAX_CHUNK_LEN` = 16 MiB — split across messages, and
|
||||
what the practical cap is for browser stacks);
|
||||
(c) flush mapping (`AsyncWrite::flush` → WS message emission point);
|
||||
(d) close mapping (WS close code → transport EOF → REQ-CH-02
|
||||
teardown; and does `AsyncWrite::shutdown` map to a WS Close frame or
|
||||
to a zero-length chunk sentinel?).
|
||||
- **Blocked on**: nothing (implementation-blocking, not
|
||||
decision-blocking — resolve during implementation of the WS adapter)
|
||||
teardown; `AsyncWrite::shutdown` maps to the zero-length EOF
|
||||
sentinel (REQ-CH-01) then a WS Close frame — confirm this ordering
|
||||
against the mux's pump-exit behavior).
|
||||
- **Blocked on**: nothing (the spike resolved the factual
|
||||
sub-questions; the remaining items are implementation decisions to
|
||||
lock during the WS adapter task)
|
||||
|
||||
### OQ-02: `/publish` body framing details
|
||||
|
||||
|
||||
@@ -112,9 +112,14 @@ WS binary message (message boundary = transport frame)
|
||||
channel N: handler-owned framing (opaque to channels)
|
||||
```
|
||||
|
||||
- **One WS binary message = one chunk** (header + payload). The WS
|
||||
message boundary is the chunk boundary — no re-splitting, no
|
||||
coalescing across messages required.
|
||||
- **WS messages are transport frames, not protocol boundaries.** The
|
||||
WS binary message stream is treated as a byte stream; the 8-byte
|
||||
chunk header is the only framing. A chunk may span WS messages and a
|
||||
WS message may carry chunk fragments — the adapter (below) is the
|
||||
seam. (In practice the write path usually emits one chunk per
|
||||
message, but nothing may depend on it: channel 0's frame writer
|
||||
issues two writes — length prefix, then body — which the mux can
|
||||
deliver as two mpsc payloads.)
|
||||
- **Channel 0's payload is the call protocol's frame format**
|
||||
(alkcall ADR-014): a 4-byte big-endian length prefix + UTF-8 JSON
|
||||
`EventEnvelope`. This is exactly the framing channel 0 uses over
|
||||
@@ -141,17 +146,23 @@ whole `Message`s. The adapter bridges the two, in both directions:
|
||||
drains the buffer. Backpressure: the reader task awaits a bounded
|
||||
buffer slot before admitting the next message (bound: OQ-01).
|
||||
- **Outbound (bytes → WS):** the `AsyncWrite` half accumulates bytes
|
||||
into a pending buffer and emits exactly one WS binary message per
|
||||
chunk — the mux writes header+payload as one contiguous
|
||||
`write_all` (the channels mux's `MuxRunner` composes chunks
|
||||
atomically), so the adapter's write-side job is buffer-until-chunk-
|
||||
complete, then flush as one message. Flush semantics and the
|
||||
chunk-completeness assumption are OQ-01 items to verify against
|
||||
alkcall's `MuxRunner` behavior.
|
||||
into a pending buffer; a background task scans the pending bytes for
|
||||
complete chunks (8-byte header → payload length) and emits each
|
||||
complete chunk as one WS binary message, carrying any partial tail
|
||||
until its chunk completes. The adapter **parses the outgoing byte
|
||||
stream** to find chunk boundaries — it does not assume one write
|
||||
equals one chunk (verified against alkcall: `write_chunk` issues
|
||||
header+payload as separate writes, and channel 0's `write_frame`
|
||||
issues prefix+body as separate writes; each can surface as separate
|
||||
mux payloads). Oversized chunks (a single chunk exceeding the
|
||||
WS-message cap, up to `MAX_CHUNK_LEN` = 16 MiB) are split across
|
||||
multiple WS messages — legal, since the receiver's boundary is the
|
||||
chunk header, not the message. Flush semantics are an OQ-01 item.
|
||||
- **Close mapping:** WS close (either side) → transport EOF → the
|
||||
demux clears all channels (REQ-CH-02: every handler sees EOF) and
|
||||
channel 0's dispatch loop fails outstanding pendings with
|
||||
`connection closed`.
|
||||
`connection closed`. `AsyncWrite::shutdown` maps to the zero-length
|
||||
EOF sentinel (REQ-CH-01) followed by a WS Close frame.
|
||||
|
||||
The adapter is shared with the `from_wss` consumer path
|
||||
([ADR-070](decisions/070-from-wss-consumer-adapter.md)) — one
|
||||
|
||||
Reference in New Issue
Block a user