diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml index a4f6d404..6de3a097 100644 --- a/.github/workflows/ci-java.yml +++ b/.github/workflows/ci-java.yml @@ -101,3 +101,31 @@ jobs: working-directory: sdks/java shell: bash run: ./gradlew build --no-daemon + + # Prove the SDK works under GraalVM native-image: run the smoke under the + # tracing agent to collect JNI/reflection/resource metadata, then build a + # native image with it and run the binary. + graalvm: + name: Java SDK GraalVM native-image + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Rust + uses: ./.github/actions/setup-rust + + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + java-version: "21" + distribution: graalvm + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Collect metadata, build, and run the native image + working-directory: sdks/java + run: | + ./gradlew -Pagent :graalvm-smoke:run --no-daemon + ./gradlew :graalvm-smoke:metadataCopy --no-daemon + ./gradlew :graalvm-smoke:nativeCompile --no-daemon + ./graalvm-smoke/build/native/nativeCompile/graalvm-smoke diff --git a/sdks/java/.gitignore b/sdks/java/.gitignore index b0c7c3d7..e7719e9f 100644 --- a/sdks/java/.gitignore +++ b/sdks/java/.gitignore @@ -1,3 +1,6 @@ .gradle/ build/ bin/ + +# Agent-generated GraalVM metadata for the smoke (regenerated in CI). +graalvm-smoke/src/main/resources/META-INF/native-image/ diff --git a/sdks/java/graalvm-smoke/build.gradle.kts b/sdks/java/graalvm-smoke/build.gradle.kts new file mode 100644 index 00000000..c72a096f --- /dev/null +++ b/sdks/java/graalvm-smoke/build.gradle.kts @@ -0,0 +1,57 @@ +// A throwaway consumer that drives the SDK's JNI + Jackson paths end to end. +// CI builds it into a GraalVM native image to verify the reachability metadata +// the runtime ships. Not published; not part of the SDK artifact. +plugins { + application + checkstyle + id("com.diffplug.spotless") version "6.25.0" + id("org.graalvm.buildtools.native") version "0.10.6" +} + +group = "org.byteveda" +version = "0.16.4" + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":")) +} + +application { + mainClass.set("org.byteveda.taskito.graalvm.Smoke") +} + +graalvmNative { + binaries.named("main") { + // Fail the build on missing metadata rather than emitting a fallback image. + buildArgs.add("--no-fallback") + } + // CI runs the smoke under the tracing agent (`-Pagent :graalvm-smoke:run`) + // then `metadataCopy` to populate this module's resources, so `nativeCompile` + // sees the JNI/reflection metadata the SDK's JNI + Jackson paths require. + agent { + metadataCopy { + inputTaskNames.add("run") + outputDirectories.add("src/main/resources/META-INF/native-image") + mergeWithExisting.set(true) + } + } +} + +spotless { + java { + target("src/**/*.java") + palantirJavaFormat("2.50.0") + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } +} + +checkstyle { + toolVersion = "10.21.4" + configFile = file("../config/checkstyle/checkstyle.xml") + isIgnoreFailures = false +} diff --git a/sdks/java/graalvm-smoke/src/main/java/org/byteveda/taskito/graalvm/Smoke.java b/sdks/java/graalvm-smoke/src/main/java/org/byteveda/taskito/graalvm/Smoke.java new file mode 100644 index 00000000..083c665c --- /dev/null +++ b/sdks/java/graalvm-smoke/src/main/java/org/byteveda/taskito/graalvm/Smoke.java @@ -0,0 +1,61 @@ +package org.byteveda.taskito.graalvm; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.byteveda.taskito.Queue; +import org.byteveda.taskito.Taskito; +import org.byteveda.taskito.events.EventName; +import org.byteveda.taskito.scheduling.PeriodicTask; +import org.byteveda.taskito.task.Task; +import org.byteveda.taskito.worker.Worker; + +/** + * Drives the SDK's JNI dispatch and Jackson (de)serialization end to end so a + * GraalVM native image of this class proves the shipped reachability metadata is + * complete. Exits non-zero on any failure. + */ +public final class Smoke { + + private Smoke() {} + + public static void main(String[] args) throws Exception { + Path dir = Files.createTempDirectory("taskito-graalvm-smoke"); + Task echo = Task.of("echo", String.class); + + try (Queue queue = Taskito.builder() + .backend("sqlite") + .url(dir.resolve("smoke.db").toString()) + .open()) { + String id = queue.enqueue(echo, "graalvm"); + + CountDownLatch done = new CountDownLatch(1); + try (Worker worker = queue.worker() + .handle(echo, (String payload) -> payload.length()) + .on(EventName.SUCCESS, event -> done.countDown()) + .start()) { + if (!done.await(20, TimeUnit.SECONDS)) { + throw new IllegalStateException("task did not complete"); + } + } + + byte[] result = queue.getResult(id).orElseThrow(() -> new IllegalStateException("no result persisted")); + String value = new String(result, StandardCharsets.UTF_8); + if (!"7".equals(value)) { + throw new IllegalStateException("unexpected result: " + value); + } + + // Exercise the Jackson DTO paths (PeriodicInfo + DeadJob deserialization). + queue.registerPeriodic( + PeriodicTask.builder("nightly", "echo", "0 0 0 * * *").build()); + if (queue.listPeriodic().isEmpty()) { + throw new IllegalStateException("listPeriodic returned empty"); + } + queue.listDead(10, 0); + + System.out.println("taskito graalvm smoke ok"); + } + } +} diff --git a/sdks/java/settings.gradle.kts b/sdks/java/settings.gradle.kts index 0b6369d4..df678ee9 100644 --- a/sdks/java/settings.gradle.kts +++ b/sdks/java/settings.gradle.kts @@ -2,3 +2,4 @@ rootProject.name = "taskito" include(":processor") include(":test-support") +include(":graalvm-smoke")