Skip to content

fix(http): make request handoff ordered and non-resumable - #5518

Open
NathanFlurry wants to merge 1 commit into
stack/fix-http-address-streaming-review-findings-pokxmroxfrom
stack/fix-http-make-request-handoff-ordered-and-non-resumable-yuplmssm
Open

fix(http): make request handoff ordered and non-resumable#5518
NathanFlurry wants to merge 1 commit into
stack/fix-http-address-streaming-review-findings-pokxmroxfrom
stack/fix-http-make-request-handoff-ordered-and-non-resumable-yuplmssm

Conversation

@NathanFlurry

@NathanFlurry NathanFlurry commented Jul 30, 2026

Copy link
Copy Markdown
Member
  • Acknowledge tunnel messages only after the actor WebSocket handoff succeeds.
  • Remove speculative HTTP request-chunk buffering and fail interrupted request streams explicitly.
  • Add regression coverage for handoff ordering and missing request starts.

@NathanFlurry

Copy link
Copy Markdown
Member Author

Stack for rivet-dev/rivet

Get stack: forklift get 5518
Push local edits: forklift submit
Merge when ready: forklift merge 5518

change yuplmssm

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review

Summary of the change: tunnel_to_ws_task.rs now defers the gateway pubsub ack for ToEnvoyTunnelMessage-kind messages until after the message has been handed to the actor's WebSocket (via the new ordered_handoff helper), so a failed WS send surfaces as an unacked failure instead of silently acknowledging data the actor never received. On the actor-host side, envoy-client's handle_request_chunk (both in tunnel.rs and actor/http.rs) now sends an explicit error/abort response instead of silently dropping a request chunk that arrives without a matching request start, so the gateway doesn't hang waiting for a response that will never come. Good fix for the underlying race, and the new ordered_handoff unit tests cleanly isolate the core ordering guarantee (reply only after handoff succeeds, no reply if handoff fails).

A few things worth a look:

1. ACK_MSG_DURATION's meaning silently changes for the common case (engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs)
For ToEnvoyTunnelMessage (i.e. actual HTTP/WS tunnel traffic, presumably the bulk of gateway messages), ack_start is now captured before parsing, but the reply/ack doesn't happen until after the full match-block processing, serialization, and conn.ws_handle.send(...) complete. That's the same span already tracked by PROCESS_MSG_DURATION (which already included the WS send pre-PR), plus parse and reply overhead. So for tunnel messages, pegboard_envoy_ack_msg_duration (documented as "Time to deserialize and reply to an incoming ToEnvoyConn msg") now overlaps almost entirely with pegboard_envoy_process_msg_duration, whereas before the two were sequential and disjoint. Any dashboard/alert tuned to ACK_MSG_DURATION as a fast "did we ack the pubsub message" signal will see it balloon and start tracking actor WS throughput instead. Worth a one-line update to the metric help text (or splitting into a distinct metric for the deferred-ack path) so this isn't a silent surprise, and worth double-checking any existing alert thresholds built on the old semantics.

2. New error path in handle_request_chunk awaits a WS send inline in the shared per-connection dispatch loop (engine/sdks/rust/envoy-client/src/tunnel.rs)
handle_request_chunk is driven from envoy_loop's single sequential message loop, shared by every actor hosted under that envoy/runner process. The new send_error_response(...).await call there follows the same pattern already used by handle_request_start's pre-existing "unknown actor" branch, so this isn't a regression, just flagging that there are now two await points in this loop that block on an outbound WS send for error paths instead of one. If that send stalls (e.g. remote flow control), the whole loop pauses for every actor on the connection, same as already happens on the actor-not-found path today.

3. Test coverage is solid for the core primitive but doesn't cover the branch selection in handle_message
The ordered_handoff tests nicely isolate the ordering guarantee itself, and the new envoy-client test confirms a stray chunk gets rejected instead of silently dropped. Nothing currently exercises that only the ToEnvoyTunnelMessage arm gets the deferred ack while ToEnvoyCommands/ToEnvoyConnPing still ack immediately in handle_message. That's understandably harder to set up (needs a real/fake Conn + UPS message), so this is a suggestion rather than a blocker.

Nothing else stood out. The reused HttpStreamAbortReasonKind::HandlerError variant and generalized send_error_response (now taking error_code/message) avoid touching the wire schema, consistent with the "never modify a published *.bare protocol version" rule. I also checked pegboard-gateway2's response-wait handler: it already treats a ToRivetResponseAbort received before any ToRivetResponseStart as a proper request failure, so sending an abort without ever sending a start (the new behavior in the two "not found" branches) is handled correctly on the receiving end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant