Skip to content
110 changes: 110 additions & 0 deletions buildSrc/src/main/kotlin/GenerateVersionProviderTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File

/**
* Gradle task that generates a SmithyVersionProvider SPI implementation for a module.
*
* When [generateInterface] is true, also generates the interface and record classes
* for modules that don't have them on their compile classpath (i.e., modules that
* don't depend on core).
*/
abstract class GenerateVersionProviderTask : DefaultTask() {

@get:Input
var moduleName: String = ""

@get:Input
var moduleVersion: String = ""

@get:Input
var generateInterface: Boolean = false

@get:OutputDirectory
var outputDir: File = project.layout.buildDirectory.dir("generated/version-provider").get().asFile

companion object {
private const val PACKAGE = "software.amazon.smithy.java.versionspi"
private const val INTERFACE_NAME = "SmithyVersionProvider"
private const val RECORD_NAME = "ModuleVersion"
private const val IMPL_NAME = "GeneratedVersionProvider"
}

@TaskAction
fun generate() {
val parts = moduleVersion.split(".")
val major = parts.getOrElse(0) { "0" }
val minor = parts.getOrElse(1) { "0" }
val patch = parts.getOrElse(2) { "0" }.replace(Regex("[^0-9].*"), "")

val packageDir = File(outputDir, "java/${PACKAGE.replace('.', '/')}")
packageDir.mkdirs()

if (generateInterface) {
File(packageDir, "$INTERFACE_NAME.java").writeText(
"""
|package $PACKAGE;
|
|public interface $INTERFACE_NAME {
| $RECORD_NAME getModuleVersion();
|}
""".trimMargin()
)

File(packageDir, "$RECORD_NAME.java").writeText(
"""
|package $PACKAGE;
|
|public record $RECORD_NAME(String moduleName, int major, int minor, int patch) implements Comparable<$RECORD_NAME> {
| @Override
| public int compareTo($RECORD_NAME other) {
| int c = Integer.compare(major, other.major);
| if (c != 0) {
| return c;
| }
| c = Integer.compare(minor, other.minor);
| if (c != 0) {
| return c;
| }
| return Integer.compare(patch, other.patch);
| }
|
| public String versionString() {
| return major + "." + minor + "." + patch;
| }
|
| @Override
| public String toString() {
| return moduleName + "=" + versionString();
| }
|}
""".trimMargin()
)
}

// Always generate the implementation
File(packageDir, "$IMPL_NAME.java").writeText(
"""
|package $PACKAGE;
|
|public final class $IMPL_NAME implements $INTERFACE_NAME {
| private static final $RECORD_NAME VERSION = new $RECORD_NAME("$moduleName", $major, $minor, $patch);
|
| @Override
| public $RECORD_NAME getModuleVersion() {
| return VERSION;
| }
|}
""".trimMargin()
)

// Always generate META-INF/services file
val servicesDir = File(outputDir, "resources/META-INF/services")
servicesDir.mkdirs()
File(servicesDir, "$PACKAGE.$INTERFACE_NAME").writeText(
"$PACKAGE.$IMPL_NAME\n"
)
}
}
17 changes: 17 additions & 0 deletions buildSrc/src/main/kotlin/smithy-java.module-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ afterEvaluate {
attributes(mapOf("Automatic-Module-Name" to moduleName))
}
}

// Generate a SmithyVersionProvider SPI implementation for this module.
if (!project.plugins.hasPlugin("software.amazon.smithy.gradle.smithy-jar")) {
val dependsOnCore = project.path == ":core" || project.configurations.getByName("compileClasspath")
.resolvedConfiguration.resolvedArtifacts.any {
it.moduleVersion.id.group == "software.amazon.smithy.java" && it.moduleVersion.id.name == "core"
}
val generateVersionProvider = tasks.register<GenerateVersionProviderTask>("generateVersionProvider") {
this.moduleName = moduleName
this.moduleVersion = smithyJavaVersion
this.generateInterface = !dependsOnCore
}
sourceSets["main"].java.srcDir(generateVersionProvider.map { it.outputDir.resolve("java") })
sourceSets["main"].resources.srcDir(generateVersionProvider.map { it.outputDir.resolve("resources") })
tasks.named("compileJava") { dependsOn(generateVersionProvider) }
tasks.named("processResources") { dependsOn(generateVersionProvider) }
}
}

// Always run javadoc after build.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
import software.amazon.smithy.java.codegen.sections.ApplyDocumentation;
import software.amazon.smithy.java.codegen.sections.ClassSection;
import software.amazon.smithy.java.codegen.writer.JavaWriter;
import software.amazon.smithy.java.core.Version;
import software.amazon.smithy.java.core.VersionCheck;
import software.amazon.smithy.java.core.serde.TypeRegistry;
import software.amazon.smithy.java.versionspi.ModuleVersion;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.OperationIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
Expand All @@ -54,28 +57,38 @@ public static void writeForSymbol(
var impl = symbol.expectProperty(ClientSymbolProperties.CLIENT_IMPL);
directive.context().writerDelegator().useFileWriter(impl.getDefinitionFile(), impl.getNamespace(), writer -> {
writer.pushState(new ClassSection(directive.shape(), ApplyDocumentation.NONE));
var template = """
final class ${impl:T} extends ${client:T} implements ${interface:T} {${?implicitErrors}
${typeRegistry:C|}${/implicitErrors}
var template =
"""
final class ${impl:T} extends ${client:T} implements ${interface:T} {${?implicitErrors}
${typeRegistry:C|}${/implicitErrors}

${impl:T}(${interface:T}.Builder builder) {
super(builder);
}
private static final ${moduleVersion:T} CODEGEN_VERSION = new ${moduleVersion:T}("codegen", ${major:L}, ${minor:L}, ${patch:L});

${operations:C|}
${impl:T}(${interface:T}.Builder builder) {
super(builder);
${versionCheck:T}.check(CODEGEN_VERSION);
}

${?implicitErrors}@Override
protected ${typeRegistryClass:T} typeRegistry() {
return TYPE_REGISTRY;
}${/implicitErrors}
}
""";
${operations:C|}

${?implicitErrors}@Override
protected ${typeRegistryClass:T} typeRegistry() {
return TYPE_REGISTRY;
}${/implicitErrors}
}
""";
writer.putContext("client", Client.class);
writer.putContext("interface", symbol);
writer.putContext("impl", impl);
writer.putContext("future", CompletableFuture.class);
writer.putContext("typeRegistryClass", TypeRegistry.class);
writer.putContext("completionException", CompletionException.class);
writer.putContext("versionCheck", VersionCheck.class);
writer.putContext("moduleVersion", ModuleVersion.class);
var versionParts = Version.VERSION.split("\\.");
writer.putContext("major", Integer.parseInt(versionParts[0]));
writer.putContext("minor", Integer.parseInt(versionParts[1]));
writer.putContext("patch", Integer.parseInt(versionParts[2].replaceAll("[^0-9].*", "")));
var errorSymbols = getImplicitErrorSymbols(
directive.symbolProvider(),
directive.model(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import software.amazon.smithy.java.codegen.generators.TypeRegistryGenerator;
import software.amazon.smithy.java.codegen.sections.ClassSection;
import software.amazon.smithy.java.codegen.writer.JavaWriter;
import software.amazon.smithy.java.core.Version;
import software.amazon.smithy.java.core.VersionCheck;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.core.schema.SchemaIndex;
import software.amazon.smithy.java.core.schema.SerializableStruct;
Expand Down Expand Up @@ -89,6 +91,7 @@ public final class ${service:T} implements ${serviceType:T} {
${builder:C|}

private static final ${schemaIndex:T} SCHEMA_INDEX = new ${generatedSchemaIndex:L}();
private static final ${moduleVersion:T} CODEGEN_VERSION = new ${moduleVersion:T}("codegen", ${major:L}, ${minor:L}, ${patch:L});

@Override
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -126,6 +129,12 @@ public final class ${service:T} implements ${serviceType:T} {
writer.putContext("typeRegistryClass", TypeRegistry.class);
writer.putContext("schemaIndex", SchemaIndex.class);
writer.putContext("generatedSchemaIndex", generatedSchemaIndex);
var versionParts = Version.VERSION.split("\\.");
writer.putContext("moduleVersion",
software.amazon.smithy.java.versionspi.ModuleVersion.class);
writer.putContext("major", Integer.parseInt(versionParts[0]));
writer.putContext("minor", Integer.parseInt(versionParts[1]));
writer.putContext("patch", Integer.parseInt(versionParts[2].replaceAll("[^0-9].*", "")));
var errorSymbols = getImplicitErrorSymbols(
directive.symbolProvider(),
directive.model(),
Expand Down Expand Up @@ -210,6 +219,7 @@ public void run() {
"""
private ${service:T}(Builder builder) {
${C|}
$T.check(CODEGEN_VERSION);
}
""",
writer.consumer(w -> {
Expand All @@ -225,7 +235,8 @@ public void run() {
"this.allOperations = $T.of(${#operations}${value:L}${^key.last}, ${/key.last}${/operations});",
List.class);
writer.popState();
}));
}),
VersionCheck.class);
}
}

Expand Down
9 changes: 8 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ dependencies {
implementation(project(":logging"))
}

jmh {}
jmh {
includes.addAll(
providers
.gradleProperty("jmh.includes")
.map { listOf(it) }
.orElse(emptyList()),
)
}

// Run all tests with a different locale to ensure we are not doing anything locale specific.
val localeTest =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.core;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import software.amazon.smithy.java.versionspi.ModuleVersion;

/**
* Measures the startup cost of the version compatibility check.
*
* <p>In production, {@code VersionCheck.check()} runs exactly once during client
* construction. This benchmark measures the per-invocation cost to quantify
* the one-time startup impact.
*/
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class VersionCheckBench {

private static final ModuleVersion CODEGEN_VERSION = new ModuleVersion("benchmark", 1, 1, 0);

@Setup(Level.Trial)
@SuppressFBWarnings(value = "LG_LOST_LOGGER_DUE_TO_WEAK_REFERENCE", justification = "Intentional for benchmark")
public void setupTrial() {
Logger.getLogger(VersionCheck.class.getName()).setLevel(java.util.logging.Level.OFF);
}

@Setup(Level.Invocation)
public void setupInvocation() {
VersionCheck.reset();
}

@Benchmark
public void versionCheckEnabled() {
VersionCheck.check(CODEGEN_VERSION);
}

@Benchmark
public void versionCheckSkipped() {
System.setProperty("smithy.java.skipVersionCheck", "true");
try {
VersionCheck.check(CODEGEN_VERSION);
} finally {
System.clearProperty("smithy.java.skipVersionCheck");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.core;

/**
* Thrown when incompatible versions of Smithy Java modules are detected on the classpath.
*/
public final class IncompatibleVersionException extends RuntimeException {
IncompatibleVersionException(String message) {
super(message);
}
}
Loading