Skip to content

refactor(stream): unify inbound dispatch via RouteInbound#293

Merged
meling merged 5 commits intomasterfrom
meling/refactory-dispatch-symmetry
Mar 21, 2026
Merged

refactor(stream): unify inbound dispatch via RouteInbound#293
meling merged 5 commits intomasterfrom
meling/refactory-dispatch-symmetry

Conversation

@meling
Copy link
Member

@meling meling commented Mar 21, 2026

Unify inbound dispatch via RouteInbound

Refactor the inbound receive path to achieve dispatch symmetry: both
server-initiated responses and client-initiated requests now flow through a
single PeerNode.RouteInbound call instead of two separate code paths in
NodeStream.

Motivation

Previously, NodeStream in internal/stream/server.go handled inbound
messages with an asymmetric two-step pattern:

  1. Call peerNode.RouteResponse(msg) to try routing server-initiated
    response IDs to pending calls; if that returns false, fall through.
  2. Call server.handler.HandleRequest(...) directly for client-initiated
    requests.

This meant the stream.Server had to carry a RequestHandler field, the
PeerNode interface exposed a boolean-returning RouteResponse method, and
AcceptPeer returned nil for untracked connections, requiring nil-guards
throughout NodeStream.

Changes

internal/stream/server.go

  • PeerNode.RouteResponse(msg) bool replaced by
    PeerNode.RouteInbound(ctx, msg, release, send), which handles both
    response delivery and request dispatch internally.
  • NewServer no longer accepts a RequestHandler argument; the handler is
    now registered at node construction time.
  • NodeStream receive loop reduced to a single peerNode.RouteInbound(...)
    call; nil-guard on peerNode in the send path removed.

internal/stream/router.go

  • New RouteInboundMessage method added to MessageRouter: the symmetric
    server-side counterpart of RouteMessage. Low-bit (client-initiated) IDs
    are dispatched to the handler; high-bit (server-initiated) IDs are
    delivered to the pending call map.
  • NewMessageRouter documentation updated to describe both uses of the
    handler.

node.go

  • newInboundNode gains a handler stream.RequestHandler parameter and
    passes it to NewMessageRouter, so the handler is stored in the router
    rather than on the server.
  • Node.RouteResponse replaced by Node.RouteInbound, which delegates to
    router.RouteInboundMessage.

inbound_manager.go

  • selfHandler field renamed to handler to reflect that it now covers all
    inbound nodes, not only the self-node.
  • All newInboundNode call sites pass the handler.
  • AcceptPeer returns a nilPeerNode value (instead of nil) for
    untracked connections, removing nil-guard complexity from NodeStream.
  • New nilPeerNode type implements PeerNode for regular clients that have
    no back-channel capability: RouteInbound dispatches to the handler (or
    calls release), and Enqueue writes directly to the stream.

server.go

  • stream.NewServer call updated to drop the now-removed handler argument.

Tests

  • inbound_manager_test.go: TestAcceptPeer updated to assert the concrete
    type returned (*Node vs *nilPeerNode) using reflect.TypeOf instead of
    a nil check.
  • internal/stream/router_test.go: new TestRouterRouteInboundMessage test
    covers all four cases (client-initiated with/without handler,
    server-initiated pending delivery, server-initiated stale absorption).
    mockRequestHandler made goroutine-safe with atomic.Bool and a done
    channel; TestRouterRouteMessage updated to use the new constructor and a
    channel-based wait instead of time.Sleep.
  • node_test.go: TestNodeRouteResponse replaced by TestNodeRouteInbound
    with four subtests mirroring the router tests.

No behaviour change

This is a pure refactor. The observable behaviour of all call paths is
identical to before; only the internal structure of the dispatch loop and the
interface surface are simplified.

meling added 2 commits March 21, 2026 17:59
Add MessageRouter.RouteInboundMessage as the symmetric server-side
counterpart of RouteMessage. On the inbound side the high bit marks
responses to this server's own calls (route to pending map) while
low-bit IDs are new client requests (return false to caller).

Apply the null object pattern to replace the nil PeerNode returned by
AcceptPeer for regular clients. The new nilPeerNode type implements
PeerNode by routing directly to the inbound stream via Send and always
returning false from RouteInbound, eliminating all peerNode != nil
branches from NodeStream.

Rename PeerNode.RouteResponse to RouteInbound and delegate Node's
implementation to the new RouteInboundMessage method. Update tests to
use server-initiated IDs (ServerSequenceNumber) where appropriate and
replace the reflect-based bool flag in TestAcceptPeer with concrete
type comparison against *Node and *nilPeerNode. Fix a data race in the
pre-existing TestRouterRouteMessage test by replacing time.Sleep with
a done channel on mockRequestHandler.
Move client-initiated request dispatch out of NodeStream and into
RouteInbound / RouteInboundMessage, so the full routing decision is
encapsulated in the router and PeerNode rather than split across
NodeStream and its callers.

Key changes:

- Store RequestHandler in MessageRouter (NewMessageRouter variadic arg)
  for both back-channel (RouteMessage) and server-side inbound dispatch
  (RouteInboundMessage); remove the handler field from stream.Server
  and the corresponding parameter from NewServer.

- Change RouteInboundMessage signature: drop bool return value, add
  ctx, release, and send parameters. Client-initiated messages are now
  dispatched to the handler (or release() called) inside the method.
  release() is always called on all code paths.

- Update PeerNode.RouteInbound interface to match: signature changes
  from (msg) bool to (ctx, msg, release, send). Node.RouteInbound and
  nilPeerNode.RouteInbound updated accordingly.

- Thread the inboundManager.handler (renamed from selfHandler) into
  newInboundNode and nilPeerNode so every inbound node dispatches
  client requests to the same handler.

- Simplify NodeStream recv loop: remove the if/continue branch; call
  peerNode.RouteInbound unconditionally for every received message.

- Fix RouteMessage argument order: ctx moved before nodeID to match
  Go convention and match RouteInboundMessage.

- Update all Route* doc comments to use consistent style with
  [Register] cross-references, and fix an incorrect inline comment
  about server-initiated IDs in RouteMessage.

- Rewrite TestNodeRouteInbound and TestRouterRouteInboundMessage as
  four subtests each, covering pending delivery, stale absorption,
  nil-handler release, and handler dispatch.
@deepsource-io
Copy link
Contributor

deepsource-io bot commented Mar 21, 2026

DeepSource Code Review

We reviewed changes in fdf6bea...5d9a0b3 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Go Mar 21, 2026 5:50p.m. Review ↗
Shell Mar 21, 2026 5:50p.m. Review ↗

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the server-side inbound stream receive path so both client-initiated requests and server-initiated responses are dispatched through a single PeerNode.RouteInbound(...) entry point, aligning server-side dispatch behavior with the existing router abstraction.

Changes:

  • Replaces PeerNode.RouteResponse(msg) bool + direct handler dispatch with unified PeerNode.RouteInbound(ctx, msg, release, send).
  • Introduces MessageRouter.RouteInboundMessage(...) to demultiplex inbound server-side messages (client-initiated requests vs server-initiated responses).
  • Updates inbound manager behavior to return a concrete nilPeerNode for untracked clients, eliminating nil-guards in NodeStream, and updates tests accordingly.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server.go Updates stream.NewServer callsite for new constructor signature (handler no longer passed).
internal/stream/server.go Removes asymmetric routing/handler path; always routes inbound via peerNode.RouteInbound and always sends via peerNode.Enqueue.
internal/stream/router.go Adds RouteInboundMessage and updates router docs to cover both inbound/server-side and outbound/client-side routing cases.
node.go Stores handler in router at inbound node construction; replaces Node.RouteResponse with Node.RouteInbound.
inbound_manager.go Renames handler field, passes handler to inbound nodes, returns nilPeerNode for untracked connections, and adds nilPeerNode implementation.
internal/stream/router_test.go Adds coverage for RouteInboundMessage and makes handler mock goroutine-safe.
node_test.go Replaces RouteResponse tests with RouteInbound tests and updates inbound node construction signature.
inbound_manager_test.go Updates TestAcceptPeer expectations to assert concrete returned type (*Node vs *nilPeerNode).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

meling added 3 commits March 21, 2026 18:29
After a stream.Send failure, gRPC cancels the stream context, but there
is a race window before ctx.Done() propagates in the NodeStream goroutine.
During that window, up to sendBufferSize additional messages can be
dequeued from the finished channel and each hits another failing Send call.

Add an atomic.Bool failed field to nilPeerNode and check it at the top of
Enqueue. The first Send error sets the flag; all subsequent calls return
immediately. The existing ctx.Done() select arm in NodeStream still handles
goroutine termination; this change only eliminates the O(buffer) wasted
gRPC send attempts in the interim.
There was a dependabot issue for grpc.
@meling meling merged commit 5968198 into master Mar 21, 2026
5 checks passed
@meling meling deleted the meling/refactory-dispatch-symmetry branch March 21, 2026 18:03
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.

2 participants