Skip to content

Styx v0.5.0 — one field, one payload story

Latest

Choose a tag to compare

@arloliu arloliu released this 08 Aug 07:02
· 6 commits to main since this release

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_MSG messages. 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 negotiated stream-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_OPEN and the single client-streaming response on STREAM_CLOSE
    are not chunked — each stays bounded by its direction's inline limit and
    fails with ErrPayloadTooLarge when 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-zero Geometry or BurstMaxPayload
    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, Transport pinned to
    TransportUDS with MaxPayload above 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
    MaxPayload fits what the connection can actually carry, and refused
    with a typed *IncompatibleError naming MaxPayload and the missing
    capability only once it genuinely does not. The error states both
    remedies directly: upgrade the plugin, or lower MaxPayload.

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.
    SendMsg or CloseSend on 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 what Err and RecvMsg report 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 to ErrOutcomeUnknown — 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. The ErrStreamAlreadyClosed documentation now
    walks every path that returns it: the open-side guard, an already-closed
    direction (including normal completion), and a concurrent CloseSend
    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=3 runs at -benchtime=500x;
    advisory numbers, not a gate). Below the per-direction inline limit, a
    send on a connection with MaxPayload set (which derives burst and
    chunking together) shows no latency regression against the same size on a
    connection with MaxPayload unset: 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.