Skip to content

Optimized inline request/reply over the Wolverine.Http transport (GH-2966) - #2990

Merged
jeremydmiller merged 2 commits into
mainfrom
feat-2966-http-inline-request-reply
May 31, 2026
Merged

jeremydmiller merged 2 commits into
mainfrom
feat-2966-http-inline-request-reply

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #2966.

When IMessageBus.InvokeAsync<T>(message) routes to a Wolverine.Http transport endpoint, the HTTP response body is the reply — no ReplyListener, no listener loop on the sender, no cross-loop TaskCompletionSource.

Sender

  • New IInlineRequestReplyEndpoint capability (Wolverine core). MessageRoute caches endpoint as IInlineRequestReplyEndpoint once at construction (routing-compile time); when present, RemoteInvokeAsync<T> sends the request and reads the reply envelope straight back instead of registering a ReplyListener — no per-message branching, null for every other transport.
  • HttpEndpoint implements the capability: serializes the request, POSTs to its OutboundUri, deserializes the reply envelope from the HTTP response body.
  • IWolverineHttpTransportClient.InvokeAsync returns (status, body) so a 500 carrying an envelope-shaped error reply is read regardless of status.
  • A receiver handler failure comes back as a FailureAcknowledgement reply and is rethrown as WolverineRequestReplyException — identical caller semantics to brokered request/reply.

Receiver

The existing /_wolverine/invoke endpoint already ran the handler and wrote Envelope.Response to the body. Now a handler exception maps to HTTP 500 + a FailureAcknowledgement envelope body (using IntrinsicSerializer.Write — WriteMessage intentionally throws). Outbox commit already happens (via InvokeInlineAsync's flush) before the response is written.

Design notes

  • Auto-activates per endpoint via the transport capability — no user-facing opt-in flag, since the routing decision is structural.
  • The brokered request/reply path is untouched (_inlineRequestReply is null for non-HTTP endpoints).
  • Fire-and-forget HTTP sends (batch/inline publish) are unchanged.

Tests

inline_request_reply_sender drives the full sender path (InvokeAsync<T> → MessageRoute inline branch → HttpEndpoint → reply) with a fake transport client standing in for the receiver. The host configures no listening endpoint, so the tests only pass because the reply is read off the response slot (otherwise they'd time out):

  • happy path returns the typed reply;
  • a receiver-side failure surfaces as WolverineRequestReplyException.

(Pre-existing unrelated flake noted: HttpTransportExecutorTests.batch_with_multiple_queues_routes_to_correct_queue fails on clean main too.)

Verification

  • New sender tests green; HTTP transport suite otherwise green.
  • dotnet build wolverine.slnx -c Release clean.

Docs & follow-up

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits May 29, 2026 20:01
…2966)

When InvokeAsync<T> routes to an HTTP transport endpoint, the HTTP response body
IS the reply — no ReplyListener, no listener loop on the sender, no cross-loop
TaskCompletionSource.

Sender:
- New IInlineRequestReplyEndpoint capability (Wolverine core). MessageRoute caches
  `endpoint as IInlineRequestReplyEndpoint` once at construction (routing-compile
  time) and, when present, RemoteInvokeAsync<T> sends + reads the reply envelope
  straight back instead of registering a ReplyListener — no per-message branching.
- HttpEndpoint implements the capability: serializes the request, POSTs to its
  OutboundUri, and deserializes the reply envelope from the HTTP response body.
- IWolverineHttpTransportClient.InvokeAsync returns (status, body) so a 500 carrying
  an envelope-shaped error reply is read regardless of status.
- A receiver handler failure comes back as a FailureAcknowledgement reply and is
  rethrown as WolverineRequestReplyException — same caller semantics as brokers.

Receiver:
- HttpTransportExecutor (the existing /_wolverine/invoke endpoint already runs the
  handler and writes Envelope.Response to the body) now maps a handler exception to
  HTTP 500 + a FailureAcknowledgement envelope body. (Uses IntrinsicSerializer.Write,
  not WriteMessage — the latter intentionally throws.)

Auto-activates per-endpoint via the transport capability; no user-facing opt-in.
Fire-and-forget HTTP sends are unchanged. Docs added (guide/http/transport.md).
Follow-up #2989 tracks full cross-process integration coverage + the CritterWatch
HTTP-only control use case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng (GH-2966)

Replaces the per-call `if (_inlineRequestReply != null)` branch in
MessageRoute.RemoteInvokeAsync<T> with polymorphism. The "send the request and
resolve the reply" step is now a protected virtual seam (sendAndAwaitReplyAsync<T>);
the base keeps the brokered reply-tracker/listener path, and a new
InlineReplyMessageRoute overrides it to read the reply straight off the transport
response (HTTP). The shape is chosen once at routing-compile time by a new
MessageRoute.For(messageType, endpoint, runtime) factory based on whether the endpoint
implements IInlineRequestReplyEndpoint, so there is no per-message if/then. All route
construction sites now go through MessageRoute.For; the factory is the only place that
news up a route, and the selection is behavior-preserving for every non-inline transport.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller

Copy link
Copy Markdown
Member Author

Updated per review: the sender path is now polymorphic instead of a runtime if.

  • The per-call if (_inlineRequestReply != null) branch in MessageRoute.RemoteInvokeAsync<T> is gone. The "send the request and resolve the reply" step is a protected virtual sendAndAwaitReplyAsync<T> seam — the base keeps the brokered reply-tracker/listener path; a new InlineReplyMessageRoute : MessageRoute overrides it to read the reply off the transport response.
  • The shape is chosen once at routing-compile time by a new MessageRoute.For(messageType, endpoint, runtime) factory (HTTP / IInlineRequestReplyEndpoint → the subclass, everything else → base). All route-construction sites now go through MessageRoute.For; it's the only place that news a route. No per-message branching anywhere.

Verified: the two inline request/reply tests pass, 41 core request/reply + routing tests pass, and dotnet build wolverine.slnx -c Release is clean.

Also filed #2994 to spec using the HTTP transport as a Wolverine cluster's node control endpoint (control queue), which builds directly on this inline request/reply work.

@jeremydmiller
jeremydmiller merged commit ef1f9e2 into main May 31, 2026
24 checks passed
This was referenced Jun 1, 2026
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.

Optimized inline request/reply over the Wolverine.Http sender transport

1 participant