Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Must use alternate model because of incompatibilities between java and javalite runtimes with well-known types #194

Merged
merged 3 commits into from
Jan 10, 2024

Conversation

jhump
Copy link
Member

@jhump jhump commented Jan 9, 2024

Refresher: the approach made use of lite and non-lite generated code in the same process (generated to different packages), so that the core client could use the lite messages, and the non-lite tests would just swap in the non-lite runtime and then do runtime-conversions to the non-lite message types (by serializing and then de-serializing).

I thought I had done enough with the previous formulation to vet that the approach would work. It compiled fine and even ran fine -- it parsed the first request from the test runner, but then blew up (as expected) from the TODO call in Client.kt.

Sadly, that was not enough. Trying to unmarshal later test cases would blow up with a class verifier error(!!!). This is because of the way classes are lazy-loaded in the JVM. So it only loaded the ClientCompatResponse.Cancel message later, when a request actually used the relevant field. And that class, generated for the lite runtime, has an incompatibility between the lite and non-lite runtimes 😢. The reason is because this message refers to google.protobuf.Empty, which extends google.protobuf.GeneratedMessageLite in the lite runtime but GeneratedMessageV3 in the non-lite runtime. Some of the generated code in Cancel compiles to a virtual call to a method of Empty that is defined on the GeneratedMessageLite super-class in the lite runtime. This results in an invokevirtual instruction in the byte code that indicates a method of GeneratedMessageLite, and that is passing Empty as the receiver. When loading that class with the non-lite runtime, the verifier sees that this is invalid because Empty does not extend GeneratedMessageLite. 🤦

So... back to the drawing board. I've now manually created POJOs for the request and relevant sub-messages, to which the lite and non-lite runtimes can adapt their generated messages. So the generated code for the lite runtime moves out of conformance/client and into conformance/client/google-javalite, since conformance/client now just refers to its own POJO models.

Unfortunately, this means more code -- both to define the POJOs and to implement conversion from the generated Protobuf types to those POJOs, which is practically the same code, but just referencing different types (lite vs. non-lite generated types). Still better than everything being forked across the two runtimes, but not as concise as I was hoping it could be.

Copy link
Member Author

@jhump jhump left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR also adds some more doc comments to the public classes in conformance/client.

freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
shadowJar {
archiveBaseName.set("shadow")
archiveFileName.set("conformance-client-java.jar")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change to make it easier to actually use the output JAR from another target in the Makefile. Without this, the actual file has a <version>-SNAPSHOT in the filename. But I couldn't find any simple way to figure out the <version>, so that a command could reference the correct filename. (It appears to be computed dynamically by Gradle based on git tags.) So this removes the suffix so we can easily reference the correct output JAR file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option here is that you can make it use the application plugin (https://docs.gradle.org/current/userguide/application_plugin.html) which when combined with installDist will create an executable script with the right jar file name embedded. I do that in protoc-gen-connect-kotlin to make it callable.

Copy link
Member Author

@jhump jhump Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦, doh! I should have done that to begin with -- good idea! I wasn't aware of the application plugin and was mainly used to building "fat jars" when doing Java dev with Maven way back in the day. That's how I ended up finding the shadowjar plugin.

I've ripped out the shadowjar stuff and converted both of these conformance programs to use the application plugin. Thanks for the pointer!

@@ -10,7 +10,7 @@ BIN := .tmp/bin
CACHE := .tmp/cache
LICENSE_HEADER_YEAR_RANGE := 2022-2023
LICENSE_HEADER_VERSION := v1.28.1
CONFORMANCE_VERSION := v1.0.0-rc1
CONFORMANCE_VERSION := v1.0.0-rc2
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main change this brings in is a new method on the conformance service: IdempotentUnary, which is just like Unary, but it is declared to be side-effect-free so it can be used with Connect GET.

@@ -49,14 +50,4 @@ class JavaInvoker(
override fun bidiStreamClient(): BidiStreamClient<*, *> {
return JavaBidiStreamClient(client)
}

companion object {
fun serializationStrategy(codec: Codec): SerializationStrategy {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this helper function into the new JavaHelpers, which contains other helper functions now that there are more things to adapt from the generated code to the model used by conformance/client.

Comment on lines -30 to -31
e.printStackTrace(System.err)
Runtime.getRuntime().exit(1)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. You left a comment about this in the previous PR, but I mistakenly only applied it to the google-javalite/.../Main.kt file and not to this one. So correcting that here.

Comment on lines +94 to +96
// Clean-up HTTP client.
httpClient.connectionPool.evictAll()
httpClient.dispatcher.executorService.shutdown()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made these changes so that the conformance client can cleanly shutdown without using System.exit. It turns out that the old code was creating lots of HTTP clients and never cleaning them up, which left non-daemon threads around, preventing the JVM from promptly terminating after the main function returned.

client: UnaryClient<Req, Resp>,
requestType: String,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter isn't used yet, but it will be needed since the lite runtime generated code doesn't actually know the protobuf names for generated types, and we need to verify that the request indicates the correct request type (since this method is re-used across all unary operations, so three different request message types: UnaryRequest, IdempotentUnaryRequest, and UnimplementedRequest).

Comment on lines +214 to +218
// okhttp *requires* that protocols contains HTTP_1_1
// or H2_PRIOR_KNOWLEDGE. So we leave 1.1 in here, but
// expect HTTP/2 to always be used in practice since it
// should be negotiated during TLS handshake,
listOf(okhttp3.Protocol.HTTP_2, okhttp3.Protocol.HTTP_1_1)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous formulation could return a set of protocols that only included HTTP_2. However, okhttp3 doesn't like this and expects HTTP_1_1 or H2_PRIOR_KNOWLEDGE to also be in the list, just in case it encounters a non-TLS request URL.

…d javalite runtimes with the wellknown types
@jhump jhump force-pushed the jh/overhaul-conformance-client branch from dcd11b6 to a76bbd7 Compare January 9, 2024 15:50
@jhump jhump marked this pull request as ready for review January 9, 2024 15:51
@jhump jhump requested a review from pkwarren January 9, 2024 15:51
Copy link
Contributor

@pkwarren pkwarren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. I like that we're testing both lite and full runtimes, but it feels pretty complex to go through all the hoops. I guess another alternative would be we could just run all the tests on the lite runtime (since the full runtime is additive) and just write some selected unit tests for the full runtime classes to verify serialization/deserialization.

I'm ok with this approach just wanted to throw out another option if this gets too tedious.

freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
shadowJar {
archiveBaseName.set("shadow")
archiveFileName.set("conformance-client-java.jar")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option here is that you can make it use the application plugin (https://docs.gradle.org/current/userguide/application_plugin.html) which when combined with installDist will create an executable script with the right jar file name embedded. I do that in protoc-gen-connect-kotlin to make it callable.

@jhump
Copy link
Member Author

jhump commented Jan 10, 2024

I guess another alternative would be we could just run all the tests on the lite runtime (since the full runtime is additive) and just write some selected unit tests for the full runtime classes to verify serialization/deserialization.

The JSON format is not supported in the lite runtime, so I think it's worth running the whole conformance suite against both runtimes just to make sure we're running the tests using JSON. But I suppose I could be convinced the other way. How about we see how it goes maintaining both, and if maintenance/fixes/etc turn out to be's a pain, we can change our mind and simplify?

@jhump jhump merged commit 2cdc745 into main Jan 10, 2024
7 checks passed
@jhump jhump deleted the jh/overhaul-conformance-client branch January 10, 2024 15:07
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