Skip to content

Routing SPI: Default forwarding to the assigned upstream broker#118

Open
hrishabhg wants to merge 6 commits into
kroxylicious:mainfrom
hrishabhg:passthrough-to-broker-proposal
Open

Routing SPI: Default forwarding to the assigned upstream broker#118
hrishabhg wants to merge 6 commits into
kroxylicious:mainfrom
hrishabhg:passthrough-to-broker-proposal

Conversation

@hrishabhg

@hrishabhg hrishabhg commented Jun 30, 2026

Copy link
Copy Markdown

Revises the Router plugin API (proposal 070) so a request on a broker-bound connection forwards to the broker that endpoint represents by default, and a router declares only the API keys it must intercept. Replaces staticRoutes().

Addresses the staticRoutes() gaps in kroxylicious#4177: the lost forward-to-assigned-broker default, the VC-level vs per-connection scope mismatch, and the unpopulatable key->route map.

SPI: intercepts(apiKey, ctx) gate (default = bootstrap connections only), shouldDecodeRequest(apiKey, ctx) (default false), and onRequest(apiKey, RequestFrame, ctx). Response decode is lazy via ResponseFrame.body(); no shouldDecodeResponse and no onResponse callback. Route binding is established at connection-creation time, which keeps bound-connection forwarding unambiguous and permits multiple routes to one cluster.

Relates-to: kroxylicious/kroxylicious#4177
Assisted-by: Claude Opus 4.8 noreply@anthropic.com

Revises the Router plugin API (proposal 070) so a request on a broker-bound
connection forwards to the broker that endpoint represents by default, and a
router declares only the API keys it must intercept. Replaces staticRoutes().

Addresses the staticRoutes() gaps in kroxylicious#4177: the lost
forward-to-assigned-broker default, the VC-level vs per-connection scope
mismatch, and the unpopulatable key->route map.

SPI: intercepts(apiKey, ctx) gate (default = bootstrap connections only),
shouldDecodeRequest(apiKey, ctx) (default false), and onRequest(apiKey,
RequestFrame, ctx). Response decode is lazy via ResponseFrame.body(); no
shouldDecodeResponse and no onResponse callback. Route binding is established
at connection-creation time, which keeps bound-connection forwarding
unambiguous and permits multiple routes to one cluster.

Relates-to: kroxylicious/kroxylicious#4177
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
@hrishabhg hrishabhg marked this pull request as ready for review June 30, 2026 09:14
@hrishabhg hrishabhg requested a review from a team as a code owner June 30, 2026 09:14
@hrishabhg hrishabhg force-pushed the passthrough-to-broker-proposal branch from 874d271 to adaed91 Compare June 30, 2026 09:14
@hrishabhg hrishabhg changed the title docs: propose default forwarding to the assigned upstream broker Routing SPI: Default forwarding to the assigned upstream broker Jul 1, 2026
@SamBarker

Copy link
Copy Markdown
Member

Thanks for writing this up — and for pushing on staticRoutes() in #4177. The problems you identified are real and the core of this proposal is the right answer.

I want to share a direction I've been thinking about as context. I haven't fully worked it out, so this isn't a counter-proposal — it's a lens I'd like us to evaluate the proposal through.

The default destination for every request is the broker the connection already represents. That's the foundation I think we should be building on. I've been calling this "identity routing" in my head — every request has a default destination (the assigned broker), and routing is about when and why you deviate from that. Whether the identity router ends up as a concrete class, a runtime behaviour, or something else, I don't know yet. But the principle feels right: the assigned broker is always the answer unless something says otherwise.

Under that lens, intercepts() is well-aligned. The default gate (ctx.virtualNode().isEmpty()) says exactly the right thing: on a bound connection there's a default destination, so don't bother the router. On bootstrap there isn't, so the router must participate. The three shapes table and the four example routers all work cleanly against this. This is the strongest part of the proposal.

Where I'd like to push is whether the decode depth mechanics need to be part of this proposal or whether they're a follow-on. Thinking through each one against the same lens:

  • shouldDecodeRequest — a router that intercepts but only needs the header (client-id, subject) shouldn't force a full decode. That's a real need. But it's a need of the interception path, not of the default destination. Rob made the point in #4177 that deserialization is a runtime decision, not a router decision. Could this be separated?

  • RequestFrame/ResponseFrame and lazy response decoding — these widen 070's sendRequest return type and introduce new abstractions. They're well thought through, but they're about optimising what happens after a router intercepts. The default destination doesn't need frames — the request just goes where it was going. Could these follow once intercepts() is in place?

I'm not saying these are wrong. I'm asking whether they're separable from intercepts(), because if they are, this proposal gets smaller and lands faster — and the decode depth work gets a proposal that can focus on it properly.

Is there a dependency I'm missing that ties the decode depth to the routing default?

I've run out of time for a deeper look right now but will come back to this properly.

Slims the proposal to the intercepts() gate per review feedback: drops
shouldDecodeRequest and the RequestFrame/ResponseFrame lazy-decode
abstractions, reverting onRequest and sendRequest to their 070 signatures.
The DecodePredicate wiring stays, consulting intercepts() so that
non-intercepted keys on bound connections are never decoded. Decode-depth
optimisation of the interception path is recorded as follow-on work.

Also updates the heading to the assigned proposal number (118).

Relates-to: kroxylicious#118
Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
@hrishabhg

hrishabhg commented Jul 2, 2026

Copy link
Copy Markdown
Author

@SamBarker

Is there a dependency I'm missing that ties the decode depth to the routing default?

No dependency — I've slimmed the proposal to just the gate and moved decode depth to follow-on work.

@SamBarker

Copy link
Copy Markdown
Member

One more thought on the decode depth side, specifically lazy response decoding.

Given the work in kroxylicious#4257, shouldDecodeResponse in DelegatingDecodePredicate is already symmetric with shouldDecodeRequest — and the router already drives response decoding declaratively for node ID translation. Metadata, FindCoordinator, DescribeTopicPartitions, Fetch (preferredReadReplica, currentLeader), and the other responses that carry node references are decoded regardless of what the router asks for, because virtual node ID mapping requires it.

Fetch is the primary response by volume and size, and it's already decoded. So lazy ResponseFrame.body() would only save decode cost for a response that a router intercepts but doesn't need to read, AND that nothing else in the pipeline (node ID translation, filters) has already decoded. Is that case real enough to justify widening sendRequest's return type and introducing an asymmetry between the request and response decode paths?

Comment thread proposals/118-default-forward-to-assigned-broker.md Outdated
Comment thread proposals/118-default-forward-to-assigned-broker.md Outdated
Comment thread proposals/118-default-forward-to-assigned-broker.md Outdated
hrishabhg added 4 commits July 6, 2026 09:26
Marks the routingMode metric categorisation as revisitable rather than fixed
by this proposal. Reworks the gate-naming open question: drops the
response-rewrite justification (that belongs to the route filter chain) and
the claim that metric and method names need not match, adopting a single
vocabulary as the goal while leaving the name choice to review.

Relates-to: kroxylicious#118
Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
Response decoding is already driven by virtual node-id translation
(kroxylicious#4257), Fetch included, leaving too narrow a case to justify
widening sendRequest; the follow-on is now request-side decode depth only.
The naming open question no longer ties itself to the unsettled metric
vocabulary.

Relates-to: kroxylicious#118
Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
Metrics are unreleased alongside 070, so the mapping is not a compatibility
concern; categorisation follows once the SPI naming settles.

Relates-to: kroxylicious#118
Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
The gate only changes request dispatch; response decoding stays governed by
filters and node-id translation (kroxylicious#4257), so the sketch no longer
models shouldDecodeResponse at all.

Relates-to: kroxylicious#118
Assisted-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
@hrishabhg

hrishabhg commented Jul 6, 2026

Copy link
Copy Markdown
Author

One more thought on the decode depth side, specifically lazy response decoding.

Given the work in kroxylicious#4257, shouldDecodeResponse in DelegatingDecodePredicate is already symmetric with shouldDecodeRequest — and the router already drives response decoding declaratively for node ID translation. Metadata, FindCoordinator, DescribeTopicPartitions, Fetch (preferredReadReplica, currentLeader), and the other responses that carry node references are decoded regardless of what the router asks for, because virtual node ID mapping requires it.

Fetch is the primary response by volume and size, and it's already decoded. So lazy ResponseFrame.body() would only save decode cost for a response that a router intercepts but doesn't need to read, AND that nothing else in the pipeline (node ID translation, filters) has already decoded. Is that case real enough to justify widening sendRequest's return type and introducing an asymmetry between the request and response decode paths?

@SamBarker Not much gain in the decode path due to nodeIdTranslation, agreed and removed from the proposal.

Two version notes:

  • NodeEndpoints/currentLeader arrive only in Fetch v16+ (Kafka 3.7).
  • PreferredReadReplica (v11+, Kafka 2.3) forces Fetch response decoding anyway — and Kroxylicious currently does not observe whether a deployment actually uses rack-aware follower fetching, so the decode can't be skipped where it's off.

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