Skip to content

Add a mechanism to avoid mixing versions for a client#1167

Open
sugmanue wants to merge 8 commits intosmithy-lang:mainfrom
sugmanue:sugmanue/avoid-version-mixing
Open

Add a mechanism to avoid mixing versions for a client#1167
sugmanue wants to merge 8 commits intosmithy-lang:mainfrom
sugmanue:sugmanue/avoid-version-mixing

Conversation

@sugmanue
Copy link
Copy Markdown
Contributor

@sugmanue sugmanue commented May 2, 2026

Issue #, if available:

Description of changes:

Other SDKs have historically suffered from issues where users inadvertently mix versions of runtime libraries in their final application. This typically happens when different dependencies pull in different versions of the same module, resulting in a classpath that contains, for example, core at version 1.1.0 alongside client-http at version 1.0.9. These mismatches cause subtle runtime errors, class not found exceptions, missing methods, or unexpected behavior, that are difficult to diagnose because nothing fails at compile time.

This change adds a version compatibility test that addresses this at runtime when the codegen schema classes are loaded: if the modules on the classpath don't all agree on the same version, or if any module is older than what the client was generated against, the test fails with a clear message identifying exactly which modules are mismatched, which prevents the classloader from succeeding.

The end-user can set the java property smithy.java.skipVersionCheck to bypass this check if needed.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@sugmanue sugmanue enabled auto-merge (squash) May 5, 2026 01:30
${?versionCheck}static {
${versionCheck:C|}
}
${/versionCheck}${#builders}${value:C|}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don’t love doing this during class initialization of code generated classes.

  1. Failures during class init are painful to debug. The first access shows up as the dreaded ExceptionInInitializerError, and every access after that turns into a NoClassDefFoundError. At that point users have to go spelunking through older logs to find the original failure, which is usually nowhere near the code path they’re currently debugging.
  2. This also does not work cleanly for cases like Dynamic Client.

My preference would be to add this to the ClientBuilder instead.

var urls = Thread.currentThread()
.getContextClassLoader()
.getResources(VERSIONS_RESOURCE);
while (urls.hasMoreElements()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can do a much cheaper check before scanning the classpath by comparing this version to the core's Version.VERSION, since in most cases if core doesn't match with the codegen version others won't match either.

fun generate() {
val dir = File(outputDir, "resources/META-INF/smithy-java")
dir.mkdirs()
File(dir, "versions.properties").writeText(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Food for thought: I think SPIs would be a better fit than a versions.properties file, especially for shaded/fat-jar cases.

With a fat jar, all the versions.properties files can get collapsed into a single file, and then you’re in “last writer wins” territory. That’s pretty fragile and also hard to notice when it breaks.

An SPI avoids that class of problem. Each dependency contributes its own service entry, and most common shading tools already know how to merge service files correctly, including Gradle Shadow and maven-shade-plugin. So instead of relying on resource overwrite behavior, we get a model that composes much more naturally under shading.

An SPI also allows us to do fancy stuff like declare methods like lastCompatibleVersion so that we can have more complex checks than version equality which would be a hedge against https://github.com/smithy-lang/smithy-java/pull/1167/changes#r3211780970


@Benchmark
public void versionCheckEnabled() {
VersionCheck.check(VERSION);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this is truly measuring what we want it to measure which is how much does this add to the cold start cost.

// All modules must report the same version.
var firstVersion = modules.get(0)[1];
for (var module : modules) {
if (!module[1].equals(firstVersion)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am a little hesitant of strict version equality specially because we follow semver and this would complicate customers deploying security patches. Like if there is a security issue in one of the modules, we force people to upgrade all of them. Not saying that might be a bad thing, just letting you know the ramifications of this decision.

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.

3 participants