Adds MaxPayload: one field that gives a plugin a large-payload story for
both unary calls and streamed messages, derived rather than hand-computed.
Added
-
Stream-chunking for oversize
STREAM_MSGmessages. A streamed message
larger than the sending direction's shared-memory inline limit used to be
a hard rejection, with no oversize route the way unary calls got from the
burst path. On a connection where the negotiatedstream-chunking
feature is active, such a message is now split into ladder-sized
fragments that ride the same shared-memory ring in order and are
reassembled on the receiving side into one logical message, delivered
whole and exactly once. Chunking is shm-only and stream-local: it never
routes over the burst socket, and the single server-streaming request on
STREAM_OPENand the single client-streaming response onSTREAM_CLOSE
are not chunked — each stays bounded by its direction's inline limit and
fails withErrPayloadTooLargewhen exceeded, feature or no feature. -
PluginSpec.MaxPayload. One field states a capacity guarantee — styx
will carry a call or a streamed message up to this many bytes — and
derives the stock shared-memory geometry, the burst-path ceiling, and the
new stream-chunking ceiling from it, so nobody hand-computes a burst
ceiling or a per-direction slab ladder anymore. A value at or below the
stock ladder's certain-fit bound derives the stock geometry alone, with
burst and chunking left off; a larger value derives all three together.
It is mutually exclusive with a non-zeroGeometryorBurstMaxPayload
on the same spec: hand-set geometry and burst ceilings keep their exact
v0.4.0 semantics, unchanged — this field is not a second way to reach the
same knobs.The guarantee is checked against the transport that actually has to carry
it, twice. Before spawning the plugin,Transportpinned to
TransportUDSwithMaxPayloadabove the uds transport's fixed frame
cap is refused with a*ConfigError. After a shared-memory attach
negotiates — once the checksum choice, and with it the connection's exact
per-direction inline limits, is known — the same requirement is checked
again against those exact limits: an old plugin that offers shared memory
but leaves burst or chunking unresolved is accepted as long as
MaxPayloadfits what the connection can actually carry, and refused
with a typed*IncompatibleErrornamingMaxPayloadand the missing
capability only once it genuinely does not. The error states both
remedies directly: upgrade the plugin, or lowerMaxPayload.
Changed
- The stream spec's provisioning text now matches the implementation
(docs/specs/stream-protocol.md§4.2). The section's sizing rules are
documented as provisioning guidance enforced through typed backpressure at
admission time, not as startup refusals; its per-side maxima are stated to
be compiled-in constants rather than operator knobs; and the one structural
check that does refuse an attach — a non-positive lifecycle queue depth —
is documented with its real ordering: validated after the region is mapped,
before anything is constructed on top of it. No wire behavior changed.
Fixed
-
A stream's terminal outcome is fully visible the moment its terminal
phase is. The engine used to publish the terminal phase first and store
the outcome detail a few instructions later, so a caller observing the
phase at exactly the wrong moment could read an incomplete outcome. The
winning terminal transition now records the whole outcome inside the same
critical section that publishes the phase, closing the window. -
Sends that lose a race with a failing terminal report the winner, not a
symptom.SendMsgorCloseSendon a stream that already terminated
with an error — or during whose send such a terminal won — used to surface
a generic cancellation. Both now return the recorded terminal outcome (the
peer status, the deadline, the crash cause), so the error a caller gets
from a send agrees with whatErrandRecvMsgreport for the same
stream. Normal completion records no terminal error, so a send after it
keeps reporting the closed-direction refusal it always did. -
Transport-shutdown errors during stream sends map to the documented
sentinels. A send that hit a closed transport used to leak an internal
error. It now maps toErrOutcomeUnknown— the bytes may or may not have
reached the peer, the same meaning that sentinel carries everywhere else —
and a send on a direction the caller already half-closed maps to
ErrStreamAlreadyClosed. TheErrStreamAlreadyCloseddocumentation now
walks every path that returns it: the open-side guard, an already-closed
direction (including normal completion), and a concurrentCloseSend
loser, each with what a caller may conclude from it.
Performance
- Measured on one machine at the release commit (
bench/stream,
-benchtime=1000x, plus repeated-count=3runs at-benchtime=500x;
advisory numbers, not a gate). Below the per-direction inline limit, a
send on a connection withMaxPayloadset (which derives burst and
chunking together) shows no latency regression against the same size on a
connection withMaxPayloadunset: 4KiB 4.70µs vs. 4.32µs, 64KiB 38.3µs
vs. 36.2µs, 1MiB− 490µs vs. 490µs. Across the repeated runs, the paired
differences were smaller than the run-to-run variance measured on the
same cell, which is the basis for calling them noise rather than a
regression. Above the inline limit, where a send can only go through
chunking:
2MiB costs 991µs and about 2.10MB/op across 34 allocations; 8MiB costs
4.14ms and about 8.40MB/op across 88 allocations. Both oversize figures
are the total cost of the derived configuration end to end —
fragmentation, the repeated underlying sends, credit and arena
bookkeeping, and the one train-owned copy together — not the cost of any
single one of those isolated.