KAFKA-7609: Add Protocol Generator for Kafka#5893
Conversation
|
Overview: buildSrc/src clients/src/main/java/org/apache/kafka/common/protocol/Message.java clients/src/main/java/org/apache/kafka/common/protocol/MessageUtil.java clients/src/main/java/org/apache/kafka/common/protocol/Readable.java, Writable.java, ByteBufferAccessor.java clients/src/main/message/README.md clients/src/main/message/*.json clients/src/main/java/org/apache/kafka/common/utils/ImplicitLinkedHashSet.java core/src, connect/src, clients/src/main/java/org/apache/kafka/clients/admin, etc. |
There was a problem hiding this comment.
This looks good to me, Colin. I left a couple minor comments.
As a more general comment, I think we should include the docstrings in the schema files, so that we can eventually generate the protocol docs off of them. And if we're going to do that, we may actually want to be more precise about not changing field names from the current manual schemas, so that people don't get thrown off by changed names. I don't think either of these things necessarily has to happen in this patch, though.
497b56e to
14d5e20
Compare
ad3de1a to
2aba254
Compare
That's a good point. I added documentation to all JSON schema files, based on the original Struct documentation. In cases where this documentation was missing, I created protocol documentation for the fields. I also ported over the comments explaining the purpose of each protocol version change.
I think it's best to decide about field names when doing the conversion for each RPC.
I fixed this so that we handle duplicates the same way we do now: by setting an INVALID_REQUEST error for them. This is actually specified in the KIP that added CreateTopics. In order to make this possible, deserialization needs to be able to represent duplicate topics in the set. I handled this by using multisets instead of sets for data received over the wire. I think this is the correct approach in general because it's flexible and will allow us to covert over the existing code more easily. Throwing a deserialization error when encountering duplicates would be very unfriendly-- deserialization errors just result in the broker disconnecting without an error message. To make this easier to review, I have split the CreateTopics changes off into a separate PR, #5972 . I think with that stuff split out, this is a very low-impact change since it's just adding a code generator, and not modifying any existing broker code path. |
bob-barrett
left a comment
There was a problem hiding this comment.
Thanks Colin! I agree about handling field names when we do the conversion. I added a couple more comments.
|
|
retest this please |
hachikuji
left a comment
There was a problem hiding this comment.
Thanks, this looks great! I left a few minor comments and some questions.
There was a problem hiding this comment.
I wonder if there should be an enum type in this schema. Looking at the generated code, this field is just represented as a byte, but it would be nice to have something friendlier to work with. Otherwise we'll just create the enum manually and do some conversion.
There was a problem hiding this comment.
Yeah, enums would be a good addition. It's a bit trickier than it seems, though. For one thing, we already have a lot of public/stable enum types, and we can't break them. enums can't inherit from other classes, either, so that escape hatch is closed.
Perhaps a simple way to make it work would be generating simple constants
public static int NONE = 0;
public static int OFFSET_OUT_OF_RANGE = 1;
... etc. etc. ...
Then the existing public/stable enums could be retrofitted to grab the values from these declarations. We could also generate classes in cases where there was no existing public thing.
47cb19c to
b585449
Compare
|
The test failure was |
|
retest this please |
|
Mentioned offline, but we need to move the few test cases under |
hachikuji
left a comment
There was a problem hiding this comment.
A few more small comments.
| this.ignorable = ignorable; | ||
| this.about = about == null ? "" : about; | ||
| if (!this.struct.fields().isEmpty()) { | ||
| if (!this.type.isArray()) { |
There was a problem hiding this comment.
Couldn't we make this check stricter? Would we ever expect the type to be something other than STRUCT?
There was a problem hiding this comment.
I refactored this code to clean it up a bit. Not all FieldSpecs describe arrays-- only some of them do. We should have a toStruct() method to make it convenient, but not embed a StructSpec unconditionally into the FieldSpec.
| import java.util.Optional; | ||
|
|
||
| public interface FieldType { | ||
| static final String STRUCT_PREFIX = "[]"; |
There was a problem hiding this comment.
nit: static final is redundant since this is an interface
| this.imports.add(newImport); | ||
| } | ||
|
|
||
| public void generate() throws Exception { |
There was a problem hiding this comment.
nit: Exception not raised. A bunch of these in MessageDataGenerator and MessageFactoryGenerator as well. Probably fall out from removing the throws on printf.
There was a problem hiding this comment.
Good point. I was able to remove quite a few of these.
| return true; | ||
| } | ||
|
|
||
| private boolean generateNonNullCheck(Versions prevVersions, FieldSpec field) throws Exception { |
| /** | ||
| * Returns the API key of this message, or -1 if there is none. | ||
| */ | ||
| short apiKey(); |
There was a problem hiding this comment.
It's a little annoying that we have to expose this for all of the nested types. Have you considered having something like an ApiMessage which extends this?
There was a problem hiding this comment.
That is a fair point. I will separate out Message as a subtype of the ApiMessage interface.
|
|
||
| public void registerMessageType(MessageSpec spec) { | ||
| if (spec.type() == MessageSpecType.REQUEST) { | ||
| if (requestApis.containsKey(spec.apiKey())) { |
There was a problem hiding this comment.
Is it worth having any validation that apiKey is not -1? I was even considering whether MessageSpec.apiKey could return Optional<Short>.
There was a problem hiding this comment.
I can use Optional there.
| } | ||
| this.fields = Collections.unmodifiableList(fields == null ? | ||
| Collections.emptyList() : new ArrayList<>(fields)); | ||
| this.hasKeys = fields == null ? false : fields.stream().anyMatch(f -> f.mapKey()); |
There was a problem hiding this comment.
nit: fields != null && fields.stream().anyMatch(f -> f.mapKey())
| // limitations under the License. | ||
|
|
||
| { | ||
| "apiKey": -1, |
There was a problem hiding this comment.
It shouldn't be needed-- I'll remove it.
In the long term, we'll want to generate other things besides ApiMessage from JSON files (enums, common structures, etc.) but we can hold off on that for now.
| /** | ||
| * A Message which is part of the top-level Kafka API. | ||
| */ | ||
| public interface ApiMessage extends Message { |
There was a problem hiding this comment.
Would it make any sense to move the version fields into ApiMessage?
There was a problem hiding this comment.
Hmm... I think it makes sense to leave them in Message. All Messages have minimum and maximum supported versions, not just ApiMessages.
|
retest this please |
|
Failing test is |
|
@cmccabe : I could find clients/src/main/message/README.md mentioned in the PR. Does that still exist? |
|
It was moved to |
| "about": "The result offsets." }, | ||
| { "name": "Timestamp", "type": "int64", "versions": "1+" }, | ||
| { "name": "Offset", "type": "int64", "versions": "1+", | ||
| "about": "The timestamp associated with the returned offset." }, |
There was a problem hiding this comment.
This comment is for Timestamp, not for Offset?
There was a problem hiding this comment.
Good catch. I have filed a PR to fix this: #6214
This patch adds a framework to automatically generate the request/response classes for Kafka's protocol. The code will be updated to use the generated classes in follow-up patches. Below is a brief summary of the included components: **buildSrc/src** The message generator code is here. This code is automatically re-run by gradle when one of the schema files changes. The entire directory is processed at once to minimize the number of times we have to start a new JVM. We use Jackson to translate the JSON files into Java objects. **clients/src/main/java/org/apache/kafka/common/protocol/Message.java** This is the interface implemented by all automatically generated messages. **clients/src/main/java/org/apache/kafka/common/protocol/MessageUtil.java** Some utility functions used by the generated message code. **clients/src/main/java/org/apache/kafka/common/protocol/Readable.java, Writable.java, ByteBufferAccessor.java** The generated message code uses these classes for writing to a buffer. **clients/src/main/message/README.md** This README file explains how the JSON schemas work. **clients/src/main/message/\*.json** The JSON files in this directory implement every supported version of every Kafka API. The unit tests automatically validate that the generated schemas match the hand-written schemas in our code. Additionally, there are some things like request and response headers that have schemas here. **clients/src/main/java/org/apache/kafka/common/utils/ImplicitLinkedHashSet.java** I added an optimization here for empty sets. This is useful here because I want all messages to start with empty sets by default prior to being loaded with data. This is similar to the "empty list" optimizations in the `java.util.ArrayList` class. Reviewers: Stanislav Kozlovski <stanislav_kozlovski@outlook.com>, Ismael Juma <ismael@juma.me.uk>, Bob Barrett <bob.barrett@outlook.com>, Jason Gustafson <jason@confluent.io>
No description provided.