Compare commits

..

1 Commits

Author SHA1 Message Date
Claude 3480874c28 client/server: in-process dispatch fast-path (opt-in)
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
When caller and callee run in the same process, a unary Call pays the
full network tax — pool.Get, dial, codec-over-socket, and the transport
pump — even though the handler table is right there. This adds an opt-in
fast-path that dispatches directly.

- internal/network: a neutral registry (transport.Message in/out) so
  client and server wire up without importing each other. A running server
  registers a dispatcher under its name on Start, deregisters on Stop.
- server: localDispatch serves a request in-process through the same
  router (identical wrappers/codecs/error mapping) over an in-memory
  socket — no dial, no pipe, no gob.
- client: LocalDispatch() opt-in. In call(), a unary request whose body and
  response are raw frames (codec/bytes.Frame — the agent/MCP/flow shape)
  dispatches locally; everything else falls back to the network path
  unchanged.

Correctness test proves the fast-path returns byte-identical replies to
the network path; benchmark shows ~545µs -> ~28µs (~20x) and ~3.6x fewer
allocations. Off by default. Covers #4817 (path b); the zero-copy typed
path remains a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
2026-07-15 15:39:36 +00:00
8 changed files with 20 additions and 51 deletions
+1 -2
View File
@@ -25,8 +25,7 @@ below is kept current between tags and rolled into the next version when it ship
- **A2A inbound AP2 mandate verification (opt-in)** — set `Options.AP2PublicKey` (or `a2a.WithPushURLPolicy`'s sibling `a2a.WithAP2PublicKey` for embedded handlers) and the gateway verifies AP2 payment/checkout mandates carried on incoming messages — signature and task/context binding — recording the outcome in each task's `ap2Verifications`, with the x402 settlement rail carried through for the paid path. Off by default; mandates are otherwise carried unverified. (`gateway/a2a/`)
- **Flow human-in-the-loop pause/resume** — a flow step can suspend a run for external input with `flow.Await(key, prompt)` (or `flow.AwaitStep`): the run checkpoints with status `waiting` and `Execute` returns cleanly. `Flow.Waiting` lists suspended runs with what they await, and `Flow.ResumeWith(ctx, runID, input)` injects the input and continues from the next step. Recovery (`ResumePending`) skips waiting runs since they need input, not a restart. (`flow/`)
- **Kubernetes reconcile core (alpha)** — `kubernetes.Reconcile(desired, observed)` decides the single action needed to converge an `Agent`/`Service`/`Flow` resource toward its Deployment (create / update / noop) and returns `Ready`/`Error` status conditions. Dependency-free (no controller-runtime / client-go) and fully unit-testable; a future operator binary supplies observed state and applies the action. (`deploy/kubernetes/`)
- **In-process "local network" fast-path (opt-in)** — `client.Local()` lets a unary `Call` to a service running in the same process skip the network transport and dispatch straight to that server's handlers (for raw `codec/bytes.Frame` bodies — the shape agent/MCP/flow tool calls use), running the same router, wrappers, and codecs. In a benchmark this cut an in-process call from ~545µs to ~28µs (≈20×) with ~3.6× fewer allocations. Off by default; falls back to the network path for anything it doesn't cover. (`client/`, `server/`, `internal/network/`)
- **`micro.Local()` service option** — turn on the in-process fast-path for a whole service in one place: every co-located unary call its client makes (agent tool calls, flow dispatch, gateway → service) takes the fast-path, with no per-call wiring. Off by default; a no-op for distributed deployments. (`service/`, root `options.go`)
- **In-process dispatch fast-path (opt-in)** — `client.LocalDispatch()` lets a unary `Call` to a service running in the same process skip the network transport and dispatch straight to that server's handlers (for raw `codec/bytes.Frame` bodies — the shape agent/MCP/flow tool calls use), running the same router, wrappers, and codecs. In a benchmark this cut an in-process call from ~545µs to ~28µs (≈20×) with ~3.6× fewer allocations. Off by default; falls back to the network path for anything it doesn't cover. (`client/`, `server/`, `internal/network/`)
### Changed
- **Remote agent chat streaming** — `micro chat` now streams replies from remote agents instead of waiting for the full response. (`cmd/micro/`, `agent/`)
+2 -5
View File
@@ -2,7 +2,8 @@
Go Micro is an **agent harness** and service framework for Go.
## Overview
**Community:** questions, ideas, or just want to build alongside us? [Join the Discord](https://discord.gg/G8Gk5j3uXr).
A harness is the runtime around an agent: the tools it can call, the memory it keeps, the guardrails that bound it, the workflows that trigger it, the services it depends on, and the protocols other agents use to reach it.
Go Micro gives you the harness as Go code. Build an agent and it gets a model, memory, tools, planning, delegation, guardrails, and service discovery; it is reachable over [MCP](https://modelcontextprotocol.io/) and [A2A](https://a2a-protocol.org). Write services and every endpoint becomes an AI-callable tool. Orchestrate the deterministic parts with durable flows. Agents, services, and flows share one runtime because an agent is a distributed system, and building one is building a service.
@@ -17,10 +18,6 @@ Go Micro gives you the harness as Go code. Build an agent and it gets a model, m
**Want to support Go Micro and see your logo here?** [Become a sponsor](https://discord.gg/G8Gk5j3uXr) — reach out on Discord.
## Community
Questions, ideas, or just want to build alongside us? [Join the Discord](https://discord.gg/G8Gk5j3uXr).
## Commercial Support
Running Go Micro in production, or building on it and want help? Paid **support, consulting, training, and retainers** are available directly from the maintainer — and they're what keep the project maintained. See [**Support**](SUPPORT.md) for the tiers, or [open a request](https://github.com/micro/go-micro/issues/new?template=commercial_support.md).
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"go-micro.dev/v6/transport/headers"
)
// localCall is the in-process fast-path for Call. When Local is enabled
// localCall is the in-process fast-path for Call. When LocalDispatch is enabled
// and the callee runs in this same process, a unary request whose body and
// response are raw frames (codec/bytes.Frame) is dispatched straight to the
// server's handlers via internal/network — no dial, no codec-over-socket,
@@ -19,7 +19,7 @@ import (
// service not registered in-process), so behavior is unchanged unless the
// fast-path fully applies.
func (r *rpcClient) localCall(ctx context.Context, req Request, resp interface{}) (handled bool, err error) {
if !r.opts.Local || req.Stream() {
if !r.opts.LocalDispatch || req.Stream() {
return false, nil
}
reqFrame, ok := req.Body().(*raw.Frame)
+10 -10
View File
@@ -81,15 +81,15 @@ func callEcho(t testing.TB, cl client.Client, msg string) EchoRsp {
return out
}
// TestLocalMatchesNetwork proves the in-process fast-path returns the
// TestLocalDispatchMatchesNetwork proves the in-process fast-path returns the
// exact same result as the network path for the same handler and request.
func TestLocalMatchesNetwork(t *testing.T) {
func TestLocalDispatchMatchesNetwork(t *testing.T) {
reg := registry.NewMemoryRegistry()
stop := startEchoServer(t, reg)
defer stop()
net := newEchoClient(reg) // network path
local := newEchoClient(reg, client.Local()) // in-process fast-path
net := newEchoClient(reg) // network path
local := newEchoClient(reg, client.LocalDispatch()) // in-process fast-path
netRsp := callEcho(t, net, "hi")
localRsp := callEcho(t, local, "hi")
@@ -102,16 +102,16 @@ func TestLocalMatchesNetwork(t *testing.T) {
}
}
// TestLocalFallsBackWhenNotLocal confirms a service not registered
// in-process still works via the network path even with Local on.
func TestLocalFallsBackWhenNotLocal(t *testing.T) {
// TestLocalDispatchFallsBackWhenNotLocal confirms a service not registered
// in-process still works via the network path even with LocalDispatch on.
func TestLocalDispatchFallsBackWhenNotLocal(t *testing.T) {
reg := registry.NewMemoryRegistry()
stop := startEchoServer(t, reg)
defer stop()
// Local is on, but the call still resolves — the fast-path only
// LocalDispatch is on, but the call still resolves — the fast-path only
// engages when it fully applies, otherwise the network path runs.
local := newEchoClient(reg, client.Local())
local := newEchoClient(reg, client.LocalDispatch())
if got := callEcho(t, local, "x").Msg; got != "echo:x" {
t.Fatalf("reply = %q, want echo:x", got)
}
@@ -136,4 +136,4 @@ func benchmarkEcho(b *testing.B, opts ...client.Option) {
}
func BenchmarkNetworkCall(b *testing.B) { benchmarkEcho(b) }
func BenchmarkLocalCall(b *testing.B) { benchmarkEcho(b, client.Local()) }
func BenchmarkLocalCall(b *testing.B) { benchmarkEcho(b, client.LocalDispatch()) }
+5 -5
View File
@@ -69,10 +69,10 @@ type Options struct {
PoolTTL time.Duration
PoolCloseTimeout time.Duration
// Local, when true, lets a unary Call to a service running in this
// LocalDispatch, when true, lets a unary Call to a service running in this
// same process skip the network transport and dispatch directly to that
// server's handlers (raw byte bodies only). Off by default.
Local bool
LocalDispatch bool
}
// CallOptions are options used to make calls to a server.
@@ -186,14 +186,14 @@ func ContentType(ct string) Option {
}
}
// Local enables the in-process fast-path: a unary Call to a service
// LocalDispatch enables the in-process fast-path: a unary Call to a service
// running in the same process dispatches straight to that server's handlers
// (skipping dial, codec-over-socket, and the transport pump) when both request
// and response bodies are raw frames (codec/bytes.Frame) — the shape agent,
// MCP, and flow tool calls use. Falls back to the network path otherwise.
func Local() Option {
func LocalDispatch() Option {
return func(o *Options) {
o.Local = true
o.LocalDispatch = true
}
}
-1
View File
@@ -8,7 +8,6 @@ var Broker = service.Broker
var Cache = service.Cache
var Cmd = service.Cmd
var Client = service.Client
var Local = service.Local
var Context = service.Context
var Handle = service.Handle
var HandleSignal = service.HandleSignal
-12
View File
@@ -111,18 +111,6 @@ func Client(c client.Client) Option {
}
}
// Local enables the in-process fast-path on the service's client: a
// unary call to another service running in the same process — agent tool calls,
// flow dispatch, gateway → service — skips the network transport and dispatches
// straight to that server's handlers (see client.Local). Off by
// default; it falls back to the network path for anything not co-located, so
// it is a pure win for all-in-one binaries and a no-op for distributed ones.
func Local() Option {
return func(o *Options) {
_ = o.Client.Init(client.Local())
}
}
// Context specifies a context for the service.
// Can be used to signal shutdown of the service and for extra option values.
func Context(ctx context.Context) Option {
-14
View File
@@ -1,14 +0,0 @@
package service
import "testing"
func TestLocalOption(t *testing.T) {
// Off by default.
if newOptions().Client.Options().Local {
t.Fatal("Local should be off by default")
}
// Enabled by the option, on the service's own client.
if !newOptions(Local()).Client.Options().Local {
t.Fatal("Local() did not enable the client fast-path")
}
}