Routing SPI: Default forwarding to the assigned upstream broker#118
Routing SPI: Default forwarding to the assigned upstream broker#118hrishabhg wants to merge 6 commits into
Conversation
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>
874d271 to
adaed91
Compare
|
Thanks for writing this up — and for pushing on 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, 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:
I'm not saying these are wrong. I'm asking whether they're separable from 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>
No dependency — I've slimmed the proposal to just the gate and moved decode depth to follow-on work. |
|
One more thought on the decode depth side, specifically lazy response decoding. Given the work in kroxylicious#4257, Fetch is the primary response by volume and size, and it's already decoded. So lazy |
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>
@SamBarker Not much gain in the decode path due to nodeIdTranslation, agreed and removed from the proposal. Two version notes:
|
| default boolean intercepts(ApiKeys apiKey, RouterContext ctx) { | ||
| return ctx.virtualNode().isEmpty(); | ||
| } | ||
|
|
There was a problem hiding this comment.
What was your thinking behind changing the name from the question the runtime asks of filters? From where I'm sitting, this gate is asking the same question as shouldDecodeRequest on the filter API — "should the runtime decode this request rather than passing it through as raw bytes?" The proposal already shows it being OR'd into the DecodePredicate, which reinforces that it's the same question asked of a different participant.
SamBarker
left a comment
There was a problem hiding this comment.
I think the proposal is good, but we need a second maintainer to have a review before we can merge.
robobario
left a comment
There was a problem hiding this comment.
Thanks for the work @hrishabhg , I'm generally supportive of this.
I wonder if we could make this more open to extension than a boolean decision and am wondering if the language in the proposal should be changed to talk less about "broker" and more about "virtual node" because future topologies may not have the "1 virtualNodeId: 1 upstreamNode" relationship.
| * frame to the connection's assigned broker (virtualNode()) without calling onRequest(). | ||
| * Default: intercept only when there is no assigned broker (bootstrap connections). | ||
| */ | ||
| default boolean intercepts(ApiKeys apiKey, RouterContext ctx) { |
There was a problem hiding this comment.
we should include short apiVersion in the signature, allowing for Routers that only need to intercept specific version ranges
| * frame to the connection's assigned broker (virtualNode()) without calling onRequest(). | ||
| * Default: intercept only when there is no assigned broker (bootstrap connections). | ||
| */ | ||
| default boolean intercepts(ApiKeys apiKey, RouterContext ctx) { |
There was a problem hiding this comment.
One idea we had in 070 was to support a richer set of actions here than intercept or forward-to-implicit virtualnode, for example you had your own ideas about forwarding on without implying a decode was required. Tom also speculated about broadcast/multicast possibilities, forwarding the same bytes to different upstreams. I wonder if we should leave the API open for extension towards that. Something like:
sealed interface RouteAction {
...
}
default RouteAction route(ApiKeys apiKey, short apiVersion, RouterContext ctx) {
if (!ctx.virtualNode().isEmpty()) {
return RouteAction.FORWARD_TO_IMPLICIT_NODE;
} else {
return RouteAction.INTERCEPT;
}
}In one way I like that this brings the implicit node target language into the API. Currently I don't think it's clear in the Router API javadoc where messages "forwarded to a route" are sent. One hesitation though is that future topologies might not have an implied node target, so adding the language might not extend well to those situations.
In future we could come up with further RouteActions like forwardToNode(VirtualNode node) implying we don't need to decode the message if we don't need to modify it or use it's contents for routing. Or multiCastToNodes and so on.
What do you think of this vs the boolean?
| instances are per-connection. A route name cannot express "forward to the broker _this connection_ | ||
| represents" — that target (`virtualNode()`) is only known per connection. |
There was a problem hiding this comment.
| instances are per-connection. A route name cannot express "forward to the broker _this connection_ | |
| represents" — that target (`virtualNode()`) is only known per connection. | |
| instances are per-connection. A route name cannot express "forward to the implied target virtual node" — that target (`virtualNode()`) is derived initially from the connection. |
I think we have to be careful with the language since we can have a DAG of Routers which will see different virtual node id mappings, so Router A may know that virtualNodeId 2 goes to route x and then Router B may see that the request is destined for virtualNodeId 0. A Router knows contextually which virtualNode() is associated with the request.
And also we don't want to suggest that there will always be a 1:1 mapping between a virtual node id and some physical upstream broker, leaving room for future topologies.
| Invert the model: the runtime forwards a bound connection's traffic to its assigned broker by | ||
| default; a router declares only what it must **intercept**. |
There was a problem hiding this comment.
generally for this and the following section I wonder if we should talk about assigned virtual node vs broker.
From each Router's point of view it is aware of VirtualNodes, the connection implies a virtual node, then it's a runtime detail that we can happen to map it 1:1 to a broker. So I wonder if the proposal should use language throughout, rather than baking in this assumption. It's pedantic I know but it might help keep the concepts clear for future readers.
| (070's shared node-id mapping, opt-in via `RouterFactoryContext.allowSharedClusterTargets()`), | ||
| which would require per-request decomposition. | ||
|
|
||
| ### Control plane vs data plane |
There was a problem hiding this comment.
not quite sure what this section is here for, is it to justify solidifying inclusion of bootstrap and broker-bound endpoints as a Proxy neccessity? Something that should be visible in our APIs, rather than just a convenience. That seems like a good thing to have noted, I don't get the relationship to the plane types.
tombentley
left a comment
There was a problem hiding this comment.
@hrishabhg thanks for this!
Do you have a POC of this, or is it purely design currently? If you had a POC I'd like to take it for a spin and try it out.
| * frame to the connection's assigned broker (virtualNode()) without calling onRequest(). | ||
| * Default: intercept only when there is no assigned broker (bootstrap connections). | ||
| */ | ||
| default boolean intercepts(ApiKeys apiKey, RouterContext ctx) { |
There was a problem hiding this comment.
We have shouldHandle*Request() and shouldHandle*Response() in the filter API, so I think a better name would be shouldIntercept or shouldHandle.
| - **Request decode depth on the interception path** — a `shouldDecodeRequest`-style declaration so a | ||
| router that routes on the header (client-id, subject) doesn't force a body decode. Separable from | ||
| the gate; deserves its own proposal. (Response-side laziness was considered and dropped: virtual | ||
| node-id translation already decodes the node-reference-bearing responses, `Fetch` included, leaving | ||
| too narrow a case to justify widening `sendRequest`.) |
There was a problem hiding this comment.
I find the idea appealing, but I'm not sure how feasible it would be given the RequestData and ResponseData APIs we have. I guess if the root object had a reference to the ByteBuf we could, in theory, deserialize progressively only when a called tried to access a struct which hadn't been decoded yet.
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