[proposal] RequestData|ResponseData API migration - #116
Conversation
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
|
|
||
| The Maven Shade Plugin takes compiled classes from `kafka-clients` and republishes them under a Kroxylicious namespace. All cross-references between relocated classes are rewritten automatically at the bytecode level. This means Kafka package reorganisations - including moves to an `internal` subpackage like KAFKA-20128 - are completely invisible. | ||
|
|
||
| A dedicated repo in the Kroxylicious GitHub org (`kroxylicious/kafka-patches`) stores the shading configuration and any generator patches needed for future enhancements. When generator enhancements are implemented, CI will apply the patches against the Kafka generator source, run the patched generator to produce enhanced `*Data` source, compile, and shade the result alongside the rest of `kafka-clients`. |
There was a problem hiding this comment.
Not sure if I have understood this right. I think the kafka-patches exist to allow us to add features to the Kafka code generator (field aliasing, for example). Have I groked this right?
Patching can get tricky and I'd worry about having it in our foundations.
I prefer the approach of forking the generator once and owning the forked code going forward. We'd rely on the serialisation/deserialisation fidelity tests to catch regressions between ourselves and Kafka.
There was a problem hiding this comment.
I think the kafka-patches exist to allow us to add features to the Kafka code generator...
Yes, they would also allow us to bypass the upstream changes we don't like (e.g. we add overloads for functions where the signature has changed).
I prefer the approach of forking the generator...
I worry that if we keep excluding upstream changes that don't affect the (de)serialized output (e.g. fixes/refactorings), eventually there may be an upstream change that both does affect the output and depends on a series of previous non-output-affecting changes, which would make the update significantly harder. I believe the proposed shading + patches approach helps mitigate this (and drift) in the best way.
|
|
||
| ### Shading configuration | ||
|
|
||
| The shading configuration in `kroxylicious/kafka-patches` uses a single broad relocation rule: |
There was a problem hiding this comment.
So would we release kafka-patches in lock step with kafka releases. If a kafka release is made which doesn't change wire level API (e.g. 4.3.1) would we still release it?
There was a problem hiding this comment.
Presumably, the proposed GitHub workflow creates a PR bumping the version, and all the tests pass straight away, so we just merge it and have another workflow automatically create the patch release. If there are conflicts, I suppose we could optionally defer until a minor release if we're busy?
|
|
||
| #### Drift in shaded classes | ||
|
|
||
| With shading, upstream changes to hand-coded classes like `MemoryRecords` and `RecordBatch` are absorbed automatically when `kafka-clients` is bumped. Drift is detected through two complementary mechanisms: |
There was a problem hiding this comment.
If Kafka changed its MemoryRecord API, we wouldn't necessarily want to ripple the change into our public API surface.
There was a problem hiding this comment.
I imagine most changes would be enhancements and fixes, no? I thought we would want to absorb most of the stuff, and we could add patches for reverting what we don't want.
|
|
||
| ### Fork the `apache/kafka` repository into the Kroxylicious GitHub org | ||
|
|
||
| Maintaining a `kroxylicious/kafka` fork with Kroxylicious-specific commits on top of upstream. This gives a standard Git workflow (edit files, commit, push) and IDE support, and Git's merge/rebase machinery handles context drift more gracefully than patches do. However, it adds a repository to the org that must be kept in sync, the fork's diff from upstream is implicit rather than inspectable, and there is a risk of the fork accumulating unintentional drift. Storing generator patches in `kroxylicious/kafka-patches` achieves the same generator-modification capability with less overhead. |
There was a problem hiding this comment.
When we were talking about forking, I understood that to mean 'just' the generator and the support classes (MemoryRecords, ApiMessage etc). I don't think any of us imagined forking the whole kafka repo.
There was a problem hiding this comment.
By "forking" I thought we meant GitHub forking, apologies. Wouldn't forking the whole repo on GitHub allow us to track changes in the files we want more easily?
…bytecode. Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
|
Thanks @piotrpdev. The shading idea is certainly an interesting approach, but I don't think we should go that way. Having the project's foundation relying on shading would worry me. I would:
If Kafka choose to refactor say MemoryRecords, we probably don't care. We don't necessarily need to take the changes into our version. We might, but we don't have to. The important thing is our MemoryRecords has a stable public API and it is interoperable. @SamBarker @robobario WDYT? |
Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
…elves. Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
…ethod renames Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Just some food for thought, one of my comment responses:
@tombentley any thoughts 👀 ? |
|
Thanks for the thorough proposal @piotrpdev, and for the prototype work backing it up. The problem statement is spot on — we need to own this namespace before 1.0, and the verification harness you've designed is valuable regardless of which approach we take. Reading through the discussion with @k-wall, I find myself coming from a similar space to Keith — my instinct is toward owning the source rather than owning a bytecode transformation. But I also see genuine value in the automatic upstream tracking that shading provides, and I don't think either of the two models I'm about to describe is exactly what's proposed in the document or what Keith suggested. I want to set them out side by side so we can have a clear conversation about the trade-offs and figure out which direction fits us best. On patchesYou'll notice neither model below includes the patch-based workflow from the proposal. Maintaining patches against Kafka's generator is a cunning solution to avoid forking it, but I don't think it pulls its weight. If we can detect drift with tooling (japicmp + fidelity tests), we don't need to stay mechanically coupled to upstream's source tree. When we need to change the generator, we fork it and own it — cleanly, not through a patch queue. The destinationI think we all agree that Kroxylicious needs to own the namespace and own the source. Personally, I'd also like us to eventually be in a position to replace key classes (like Both models below reach full ownership. They differ in what order they get there and what you're carrying at each stage. Model A: Shade first, own incrementally
What it gives us: The cheapest possible step 1 — namespace ownership with almost no source to maintain. Automatic upstream tracking until we choose to take over each class. What it costs: We carry a bytecode transformation as our foundation until step 3. And step 2 is the same work as Model B's step 1 — deferred, not avoided. What it gets right: Non-wire behavioural changes (bug fixes, validation improvements, etc.) are absorbed automatically. We're not trying to fork Kafka — we want to be additive to upstream. We own the namespace and the wire contract, and we layer our own enhancements on top (e.g. field aliasing to maintain API compatibility when upstream renames things). Model B: Own the source from the start
What it gives us: Full source ownership from day one. Every upstream change is a deliberate choice — review, cherry-pick, skip. Generator enhancements and buffer replacements are the same kind of work (modify source we own). Uniform handling of all change types. What it costs: More manual effort to absorb upstream changes. Risk of drift if we neglect upstream tracking — though tooling mitigates this (see below). Drift detection for either model
Combined with the wire-level fidelity tests from the proposal, this gives us two complementary signals: japicmp catches API-level divergence, fidelity tests catch wire-format divergence. We already use japicmp in the main build, so the plugin infrastructure is there — it's just a matter of configuring a comparison module. As long as we have wire compatibility, we're insulated from the need to absorb every API change as it happens. The sequencing questionThe real question isn't which model is "right" — it's which sequence of intermediate states we're comfortable carrying. Model A gives the cheapest step 1 but means step 2 is a transition (from shading to source ownership for the generated classes). Model B front-loads more work but every subsequent step is the same kind of work: modify source we own. I'm also not sure how much step 1 of Model A actually saves over Model B. The non-generated classes (~50 or so) are relatively stable — copying them in as source with japicmp tracking drift might be nearly as cheap as shading them, without the bytecode-transformation dependency. What do people think? I'm genuinely open — I see the appeal of both paths and I'd like to hear where others land. |
|
We've been sitting without any progress for a few weeks, so I want to give a clearer steer rather than leaving the question open. My previous comment deliberately laid out Model A and Model B as neutral alternatives to start a debate. However, since there's been no further input from anyone, I'm going to take a position. Model A's real advantage is speed: less upfront work, faster to absorb a new Kafka release. That's a genuine benefit and I wanted to give space for others to argue for it. But in the absence of that argument, I think Model B is the stronger foundation. It gives us uniform handling of all change types from day one, avoids carrying a bytecode transformation we'll eventually unwind anyway, and generator enhancements are enhancements we can now make rather than patches layered on top. My recommendation: Model B — own the source from the start. Concretely, that means:
I realise this is a significant change to the proposal's direction. Would you be open to reworking it along these lines, @piotrpdev? Happy to talk through it on a call if that would help — reshaping a proposal after review is normal and this has been a really valuable exploration of the problem space. |
Co-authored-by: Sam Barker <sam@quadrocket.co.uk> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
|
@SamBarker done in |
|
@piotrpdev can you update the PR description to describe the current approach? |
tombentley
left a comment
There was a problem hiding this comment.
A few nits, but over all I'm happy with this.
|
|
||
| | Component | Source | Approach | | ||
| |--------------------------------------------|-------------------------------------------|--------------------------------------------------------| | ||
| | `MessageGenerator` + friends (~35 files) | `kafka/generator/src/` | Copied into Kroxylicious repo as a new module | |
There was a problem hiding this comment.
"as a new module": let's decide what it's called.
There was a problem hiding this comment.
Decided on kroxylicious-kafka-message-generator for the generator and kroxylicious-kafka-common for the rest (we copy Kafka's package structure), open to suggestions though 😃.
|
|
||
| ### Enhancements to the generator | ||
|
|
||
| #### Type-safe request-response pairing |
There was a problem hiding this comment.
Personally I think this is just about worth doing. On the one hand it's a bit cute and doesn't offer a lot of extra type safety. But it also doesn't seem particularly hard or risky.
There was a problem hiding this comment.
Let's keep focus on what we have to do. This proposal is big enough already. If we decide we want to propose it separately, after this proposal is delivered, but before the release, fine.
There was a problem hiding this comment.
Agree with @k-wall — this is a good idea but let's keep it out of scope for this proposal. The goal here is to take ownership of the protocol surface and unblock Kafka version upgrades. Type-safe pairing can be a follow-up once the foundation is in place.
There was a problem hiding this comment.
Type-safe pairing can be a follow-up once the foundation is in place.
Not without breaking compatibility again. We if we want to do this separately we'd need to land a PR for it in the same release.
|
I'm currently working on kroxylicious/kroxylicious#4391. The code which I have currently uses some of Apache Kafka's internal classes in
Thoughts? |
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Hmm, is that not a similar case to |
| | Component | Source | Approach | | ||
| |---------------------------------------------|-------------------------------------------|--------------------------------------------------------| | ||
| | `MessageGenerator` + friends (~35 files) | `kafka/generator/src/` | Copied into `kroxylicious-kafka-message-generator` | | ||
| | `*Data` classes (~198) | IDL specs + copied `MessageGenerator` | Generated into `kroxylicious-kafka-common` | |
There was a problem hiding this comment.
So kroxylicious-api is going to depend on kroxylicious-kafka-common. Curious - did you consider running the generator in kroxylicious-api instead? I'm wondering if we really need the kroxylicious-kafka-common module?
There was a problem hiding this comment.
I decided against it because kroxylicious-api looks to entirely consist of interfaces and very simple/short classes, so I thought putting implementation-heavy code in there would be a bad idea. Let me know if this is an incorrect assumption though.
|
|
||
| This gives full source ownership from day one. Every upstream change is a deliberate choice - review it, absorb it, or skip it. Generator enhancements (type-safe request-response pairing, Javadoc from IDL, `aliases`) are first-class commits to the in-repo generator, not patches layered on top. And when the time comes to replace buffer/record classes with Netty-native implementations, there is no bytecode transformation to unwind - we already own the source. | ||
|
|
||
| The `kroxylicious-krpc-plugin` currently depends on `kafka-clients` solely for `ApiKeys`. After this change it depends on the new artifact instead (or we could codegen an `ApiKeys` from the RPC definitions themselves). Its role in generating filter interfaces, invokers, and decoders is otherwise unchanged. |
There was a problem hiding this comment.
After this change [kroxylicious-krpc-plugin] depends on the new artifact instead (or we could codegen an
ApiKeysfrom the RPC definitions themselves). Its role in generating filter interfaces, invokers, and decoders is otherwise unchanged.
Either way it sounds a bit ugly.
I think eliminating the use of ApiKeys class from KrpcGenerator mechanism itself will simplify the problem. From a quick look, I think its only use-case is mapping id to name. The Kafka RPC definitions include the ids and names, so I think it should be an easy refactor.
{
"apiKey": 1,
"type": "request",
"listeners": ["broker", "controller"],
"name": "FetchRequest"
}There was a problem hiding this comment.
Good idea, but I'd keep it out of scope for this proposal unless it simplifies the package move itself. If eliminating the ApiKeys dependency from KrpcGenerator makes the migration easier — fewer things to relocate, fewer cross-module dependencies to untangle — then it's worth doing as part of this work. If it's just a nice cleanup, it can come after.
|
|
||
| The non-generated classes (`MemoryRecords`, `RecordBatch`, protocol infrastructure, etc.) are copied source that may diverge from upstream over time as Kafka evolves. Drift is detected through two complementary mechanisms: | ||
|
|
||
| 1. **`japicmp` API comparison:** `japicmp-maven-plugin` compares by fully-qualified class name, so we cannot point it directly at our jar and `kafka-clients` - the package names do not match. Instead, we shade `kafka-clients` with the same `org.apache.kafka` → `io.kroxylicious.kafka` relocation into a throwaway reference artifact, then run `japicmp` comparing our source-built jar against that reference. Both jars now have matching package names, and `japicmp` reports exactly which APIs have diverged from upstream - method renames, signature changes, removed types. Kroxylicious already uses `japicmp` in the main build, so the plugin infrastructure is in place. |
There was a problem hiding this comment.
I question whether we really need this. The important thing is wire level compatibility.
There was a problem hiding this comment.
I think wire compatibility is necessary but not sufficient on its own. These are Kafka's internal classes — Kafka doesn't consider them public API and won't reliably document changes to them. The 4.3.0 record.* move that motivated this proposal is a good example: even reading the JIRA (https://issues.apache.org/jira/browse/KAFKA-20128) you wouldn't pick it as a class relocation — you'd have to read the actual PR (apache/kafka#21412) to connect the dots. No reasonable level of release note monitoring would have caught it proactively.
japicmp is the only automated signal for structural drift that isn't wire-visible — a renamed method, a changed signature, a removed type. Without it, the alternative is manually diffing our copies against upstream, which won't happen consistently.
That said, it's a transitional safety net. As each copied class gets replaced with a Kroxylicious-native implementation, we stop running japicmp against it — we've deliberately diverged at that point. But for classes that are still essentially Kafka's code, the drift signal remains valuable. The scope shrinks progressively, class by class.
|
Could we add a high level implementation plan sketching out the path to being done? We will want to chunk this work up, so let's think about it up-front. |
|
Building on the review comments from @tombentley (module naming) and @k-wall (module structure, whether a separate module is needed, where tests live), I think the proposal would benefit from spelling out the module structure more concretely. The current text says "copied into Kroxylicious repo as a new module" and "invoked during the build" but does not cover the relationships between modules, how the generator is invoked, or where the generated source and tests land. One way this could look:
I think making this structure explicit in the proposal would address several of the open questions — @k-wall's points about whether a separate module is needed and where tests live, and @tombentley's question about the module name. @piotrpdev what do you think? If you see a better structure or have concerns about any of this, happy to discuss. |
|
I think this is out of scope for this proposal — 116 is about owning the types that appear in the Kroxylicious API surface. Filters are a separate domain and are free to depend on For your specific case, option 3 (shared module) actually makes sense to me. It's not just your SASL termination work — we have SASL initiation alongside the existing OAUTHBEARER usage as well, so there's a natural set of consumers for those security classes. That said, I'd keep it as a separate effort rather than adding it to 116's scope. |
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
|
@SamBarker Thanks, I mentioned/named the modules more explictly in Also, I mentioned using |
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Piotr Płaczek <piotrpdev@gmail.com>
Done in |
Summary
Eliminate Kroxylicious's exposure to Kafka internal package re-organisations by taking full source ownership of the Kafka protocol classes, delivering a stable public API surface before 1.0.
Approach
Kafka's
MessageGenerator(~35 files) and the non-generated support classes (protocol.*,record.*, scatteredcommon.*types) are copied into the Kroxylicious repo as source underio.kroxylicious.kafka.*, retaining their original Apache 2.0 licence headers. The generator is run at build time against the upstream IDL specs (bundled inkafka-clients) to produce the*Dataclasses:aliases, Javadoc from IDL) are first-class commits, not patchesjapicmpdrift detection:kafka-clientsis shaded into a throwaway reference artifact with matching package names;japicmpthen reports any API divergence between our classes and upstreamkafka-clientsversion; a new GitHub Workflow regenerates*Dataclasses and runs fidelity tests andjapicmpon the PR, reporting any incompatibility immediatelyKey Benefits
Migration
Filter developers update all
org.apache.kafka.*imports toio.kroxylicious.kafka.*. Ased-based migration script and package mapping guide will be provided alongside the change.Relates-to: kroxylicious/kroxylicious#3996