Adapter Dispatch-Core Unification — adapters/mqtt, adapters/mqtt5, adapters/zeromq¶
Status: Idea / investigation only — Phase 1 scoped as an AUDIT, not a redesign. No code changes proposed yet. ← Back to Roadmap
Motivation¶
Spun out of a retrospective discussion (not a feature request) about
whether this project's bottom-up, codec-first design approach (declare
route/channel shape → attach middleware → now Router) produced clean
fundamentals, or accumulated "layers of cope" along the way. The honest,
evidence-grounded answer: the api/* declaration layer (codecs, builders,
opts) has stayed comparatively clean — proven by how fast
d-0008-declarative-router-groups.md's own design
closed, reusing already-validated primitives at every step. The recurring,
EXPENSIVE failure mode in this project's history has instead been in the
ADAPTER-layer dispatch plumbing — confirmed by re-reading this project's
own design docs directly, not by re-deriving the claim from scratch:
docs/design/d-0001-rest-middleware-workflow-simplification.md's "Lessons Learned" (7 numbered findings): a reflection-based dispatch mechanism claimed "equivalent" to the old one silently was NOT (missing Formats negotiation, missingErrorResponseFor) — undetected until 128+ old tests were migrated wholesale onto the new entry point.docs/design/d-0007-declarative-middleware-layering.md's "Post-ship regression found and fixed": events' Subscribe dispatch had THREE separately-maintained reflection-based implementations —adapter.go's genericmakeSubscribeMessageHandler[T](exercised ONLY by the low-level escape hatch and unit tests),transport.go's reflection-basedSubscribe(theports.Patternbinding path), andcaller.go's (mqtt5/mqtt) /serve_subscribers.go's (zeromq) reflection-basedServeSubscribers— the ACTUAL, documented, recommendedClient.Attachworkflow every example/tutorial tells a user to use. That third path had NOMiddlewareHandlersdispatch of any kind, silently, for the entire lifetime of Rollout Phase B — found only when a NEW example was written specifically to exercise that exact entry point. The doc's own words: "this is the THIRD time this exact class of gap has surfaced in this doc's own history."
This recurring pattern — a capability correctly designed at the api/*
layer, but silently missing from ONE of several independently-maintained
adapter dispatch paths, undetected until a real consumer exercises that
EXACT entry point — is the concrete, named problem this roadmap doc
investigates.
Confirmed asymmetry: REST already converged; events (confirmed) and reqreply (uncatalogued) did not¶
| Pattern | Dispatch paths confirmed | Converged? |
|---|---|---|
api/rest (adapters/nethttp) |
ONE — serve.go's ServeOne/Serve reflects (reflect.Value.Call/MethodByName) directly into RouteHandle[Req,Resp]'s OWN generic, type-safe methods (ApplyMergeFields, DecodeMerged, EncodeResponseMergeFields, ErrorStatusFor, etc.) — confirmed via its own doc comment: "runs the SAME pipeline... invoked via reflect.Value.Call." |
YES — the reference pattern to converge toward |
api/events Subscribe (adapters/mqtt/mqtt5/zeromq) |
THREE, confirmed directly from d-0007's own regression writeup (cited above) |
NO — confirmed broken, since fixed for the specific GrantedScopes/ContextField gap that surfaced it, but the 3-implementation STRUCTURE itself was not consolidated into one |
api/events Publish |
Not yet audited this round — adapters/mqtt5/transport_dispatch.go's own doc comment confirms SOME ad hoc convergence already happened ("the SUBSCRIBE-side equivalents ALREADY EXIST in caller.go... this file reuses them directly rather than duplicating"), but the exact count/location for Publish specifically is unconfirmed |
UNKNOWN |
api/reqreply Call/Serve (adapters/mqtt5/zeromq) |
Structurally SEPARATE files from events' own dispatch (reqreply_transport.go vs. transport.go) per adapter, confirmed via file listing — whether reqreply's OWN Call/Serve paths have the same "3 independent implementations" shape as events' Subscribe, or whether they already converged, is unconfirmed |
UNKNOWN |
Scope decisions (Phase 1 — THIS doc)¶
| In scope (Phase 1) | Out of scope |
|---|---|
AUDIT ONLY: catalog the exact count and location of independent dispatch implementations per capability (Security, bound Middleware, GrantedScopes, ContextField, Capabilities/Disposition, ErrorPattern/DeadLetter), per adapter (mqtt, mqtt5, zeromq), per pattern (events Publish — unaudited; reqreply Call/Serve — unaudited) |
Any actual consolidation/redesign of the dispatch mechanism itself — deferred to a follow-up round once the audit's true scope is known |
| Producing a literal audit table (capability × adapter × pattern → implementation count + file:line) so a future round can SEE the full blast radius before committing to a design | Redesigning reflection vs. codegen vs. any other dispatch TECHNIQUE — this doc does not take a position on reflection itself being the problem (REST's own converged, single-path design ALSO uses reflection — confirmed above — so reflection per se is not the issue) |
| Confirming/denying whether REST's OWN convergence pattern (one reflection path, calling into the handle's own generic methods) is a viable template events/reqreply could converge toward | Touching api/* — confirmed clean in this round's own retrospective; this is adapter-layer only |
Cross-referencing d-0008-declarative-router-groups.md to confirm Router is UNAFFECTED (see below) |
adapters/websocket/adapters/redis/adapters/file/adapters/sql — out of scope, no evidence yet that they share this shape (would need their own audit if a driver appears) |
Why d-0008-declarative-router-groups.md's Router is NOT at risk of this regression class¶
Confirmed structurally, not just asserted: Router (all 3 patterns, fully
designed) is PURELY an api/*-layer, spec-ASSEMBLY-time construct — its own
"Observer integration" section states plainly that "once Register()
succeeds, the resulting handle is indistinguishable from one built without a
Router at all" and "every existing Observer call site downstream (adapter
dispatch) is completely unaware a Router was ever involved." Router never
touches dispatch, so it cannot introduce a 4th independently-maintained
dispatch path, or silently miss wiring a capability into one of the existing
ones — it composes opts/prefixes BEFORE any of the 3 (or however many)
existing dispatch paths ever run, using each leaf's own, completely
unchanged Register/Handle method.
Audit plan — what Phase 2 (a follow-up round) would need to answer¶
- For EACH of events' Publish side (mqtt/mqtt5/zeromq) and reqreply's
Call/Serve side (mqtt5/zeromq): does the SAME "3 independent dispatch
implementations" shape exist, or did it already converge (like
transport_dispatch.go's own comment suggests partially happened for events' Subscribe reflection helpers)? Produce a literal table: capability → file:line of EVERY place that capability's dispatch is implemented, per adapter, per pattern. - For any confirmed-duplicated capability, is REST's OWN converged pattern (reflect into the Route/Channel/RouteHandle's own generic methods, ONE reflection entry point per direction) mechanically applicable, or does events'/reqreply's protocol shape (pub/sub fan-out; async request/reply correlation) genuinely require a structurally different dispatch shape per entry point?
- What is the actual TEST-COVERAGE gap per entry point today — i.e. for
each of the 3 (or more) dispatch paths per capability, is there a test
exercising that EXACT entry point (not just "a" test of the capability
via the easiest-to-test path)? This is the concrete, actionable
prevention measure
d-0007's own regression writeup names as what would have caught it earlier ("a new mechanism is 'shipped' only once a test or example exercises it through the SAME entry point real users are told to use"). - Is a shared, EXPLICIT dispatch-core abstraction (e.g. one function per
capability that EVERY entry point calls through, rather than 3
independently-reasoned reflection blocks) mechanically feasible given
each entry point's different starting point (fully-generic
Tknown at compile time inadapter.go; type-erasedanyrecovered via reflection intransport.go/caller.go)?
Out of scope (deferred, no driver yet)¶
- Any actual code change — this doc is audit-scoping only.
- Performance/benchmarking of reflection-based dispatch — not the question this doc investigates (correctness/consistency is).
- Other adapters (
websocket,redis,file,sql) — no evidence yet they share this specific "N independent per-entry-point reflection implementations" shape; would need their own audit if a driver surfaces.
Open questions (not yet answered — Phase 2's job)¶
- Does the Publish side of events and the Call/Serve side of reqreply share Subscribe's exact "3 independent implementations" shape, or is Subscribe's case worse/better than its siblings?
- Is a shared dispatch-core abstraction mechanically achievable across
compile-time-generic and runtime-reflected entry points, or does the
type-erasure boundary (confirmed real:
transport.go/caller.gorecoverTonly viareflect.ValueOfagainst a type-erased*ChannelHandle) force genuine duplication no matter how it's organized? - Should the prevention measure be PROCESS (a checklist requirement: "a new capability's roadmap doc's Unit Test Plan MUST include one test per confirmed dispatch entry point, not just one test of the capability"), ARCHITECTURE (consolidate to fewer paths), or BOTH?