Two things the corpus runner got wrong about the engine #40
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| MAVEN_ARGS: -B -ntp | |
| jobs: | |
| # The API artifact is what a caller compiles against, it has no native | |
| # code in it, and it is the one thing that has to build on every JDK | |
| # this client claims to support. It needs no engine, so it answers in | |
| # under a minute and it answers first. | |
| api: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| java: ["17", "21", "25", "26-ea"] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: ${{ matrix.java }} | |
| cache: maven | |
| # Only the API module, because the FFM provider compiles to release | |
| # 25 and a JDK 17 compiler cannot be asked for that. A caller on 17 | |
| # gets exactly this artifact and the JNI provider beside it. | |
| - run: mvn $MAVEN_ARGS -pl zudb -am test | |
| # The Arrow reader is a 17 artifact as well, and the README says so | |
| # in a table. This is what keeps that true. Its tests need the FFM | |
| # provider to run against, which this JDK cannot build, so what is | |
| # checked here is that the sources a 17 caller compiles against | |
| # compile on 17. | |
| - run: mvn $MAVEN_ARGS -pl zudb -am install -DskipTests | |
| - run: mvn $MAVEN_ARGS -pl zudb-arrow compile | |
| # The whole client against the engine at its own HEAD, which is what | |
| # makes a red job here mean the binding is wrong about the ABI rather | |
| # than that a checked-in copy of something is stale. | |
| engine: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| java: ["25", "26-ea"] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: tamnd/zu | |
| path: engine | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: ${{ matrix.java }} | |
| cache: maven | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: engine | |
| # ZU_ABI_VERSION is a header macro rather than a symbol, so a | |
| # binding with no C compile step has nowhere to read it from and | |
| # has to write it down. This is the step that stops the written | |
| # down copy from drifting. | |
| - name: The ABI this client speaks is the ABI the engine offers | |
| run: | | |
| set -eu | |
| engine_abi="$(sed -n 's/^#define ZU_ABI_VERSION "\(.*\)"$/\1/p' \ | |
| engine/crates/zu-capi/include/zu.h)" | |
| client_abi="$(sed -n 's/.*ABI_VERSION = "\(.*\)";.*/\1/p' \ | |
| zudb/src/main/java/dev/zudb/Zu.java)" | |
| test -n "$engine_abi" | |
| test -n "$client_abi" | |
| echo "engine $engine_abi, client $client_abi" | |
| test "$engine_abi" = "$client_abi" | |
| # The shim is written against this header and opens a libzu at run | |
| # time, so a copy that has drifted compiles clean and then reads a | |
| # struct that moved. This is the step that stops it drifting. | |
| - name: The vendored header is the engine's header | |
| run: diff -u engine/crates/zu-capi/include/zu.h zudb-jni/src/main/c/zu.h | |
| - name: Build libzu | |
| working-directory: engine | |
| run: cargo build --release -p zu-capi | |
| - name: Where the library landed | |
| run: | | |
| set -eu | |
| lib="$(ls engine/target/release/libzu.dylib engine/target/release/libzu.so 2>/dev/null | head -1)" | |
| test -n "$lib" | |
| echo "ZU_LIBRARY=$GITHUB_WORKSPACE/$lib" >> "$GITHUB_ENV" | |
| # One platform, this runner's, which is all a runner can build and | |
| # all the suite here needs. The other six are built in the release, | |
| # on runners of their own. | |
| - name: Build the JNI shim | |
| run: ./scripts/build-shim.sh | |
| - run: mvn $MAVEN_ARGS test | |
| # The suite again with assertions on everywhere, including the ones | |
| # in the JDK itself. The bounds checks a MemorySegment does are the | |
| # difference between a wrong offset failing and a wrong offset | |
| # reading somebody else's memory. | |
| - run: mvn $MAVEN_ARGS test -Dzu.test.args="-ea -esa" | |
| # Not for the numbers, which mean nothing on a shared runner, but | |
| # because a benchmark is code that nothing else compiles and | |
| # nothing else runs. One iteration is enough to say it still works. | |
| - run: mvn $MAVEN_ARGS -DskipTests package | |
| - run: java -jar zudb-bench/target/benchmarks.jar -f 1 -wi 1 -i 1 -r 1s -w 1s | |
| # The cross client corpus, run against the engine this job builds. | |
| # Every client answers the same fourteen hundred cases and a report is | |
| # diffed line for line against the other four, so this is the job that | |
| # says this client agrees with them rather than only with itself. | |
| # | |
| # The cases come from the same checkout the library was built from, | |
| # which is the pairing that makes the report mean anything. A corpus | |
| # ahead of the library reports the engine catching up to its own cases | |
| # as this client failing, and a library ahead of the corpus reports | |
| # nothing at all. This client builds the engine rather than shipping an | |
| # archive of it, so both are the same checkout and there is no revision | |
| # to pin. | |
| # | |
| # A job of its own rather than a step in the one above, because the run | |
| # is fourteen hundred databases and the job above runs its suite three | |
| # times. | |
| corpus: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: tamnd/zu | |
| path: engine | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| cache: maven | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: engine | |
| - name: Build libzu | |
| working-directory: engine | |
| run: cargo build --release -p zu-capi | |
| - name: Where the library landed | |
| run: | | |
| set -eu | |
| lib="$(ls engine/target/release/libzu.so)" | |
| test -n "$lib" | |
| echo "ZU_LIBRARY=$GITHUB_WORKSPACE/$lib" >> "$GITHUB_ENV" | |
| # Verbose, because the run logs every case the engine has not | |
| # caught up to and a release branch is read for exactly that. | |
| - run: mvn $MAVEN_ARGS -pl zudb-corpus -am test -Dsurefire.useFile=false | |
| env: | |
| ZU_CASES: ${{ github.workspace }}/engine/conformance/cases | |
| # The JNI provider on the JDKs it exists for. Panama is not there on | |
| # 17 or 21, so on those two this is the only way to call the engine at | |
| # all, and a client that claims 17 and is only ever tested on 25 is a | |
| # client that claims 17. | |
| # | |
| # It runs the same cases the Panama provider runs, out of the same | |
| # artifact, so a difference between the two providers is a red job here | |
| # rather than something a user finds. | |
| jni: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| java: ["17", "21"] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: tamnd/zu | |
| path: engine | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: ${{ matrix.java }} | |
| cache: maven | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: engine | |
| - name: The vendored header is the engine's header | |
| run: diff -u engine/crates/zu-capi/include/zu.h zudb-jni/src/main/c/zu.h | |
| - name: Build libzu | |
| working-directory: engine | |
| run: cargo build --release -p zu-capi | |
| - name: Where the library landed | |
| run: | | |
| set -eu | |
| lib="$(ls engine/target/release/libzu.dylib engine/target/release/libzu.so 2>/dev/null | head -1)" | |
| test -n "$lib" | |
| echo "ZU_LIBRARY=$GITHUB_WORKSPACE/$lib" >> "$GITHUB_ENV" | |
| - name: Build the JNI shim | |
| run: ./scripts/build-shim.sh | |
| # Named modules rather than the whole reactor, because the Panama | |
| # provider compiles to release 25 and this JDK cannot be asked for | |
| # that. What is left is exactly what a caller on 17 gets. | |
| - run: mvn $MAVEN_ARGS -pl zudb,zudb-tck,zudb-jni -am test | |
| # Again with every assertion on, including the JDK's own. A wrong | |
| # length handed to NewDirectByteBuffer is the difference between a | |
| # test that fails and a test that reads somebody else's memory. | |
| - run: mvn $MAVEN_ARGS -pl zudb,zudb-tck,zudb-jni -am test -Dzu.test.args="-ea -esa" | |
| # The claim the zudb-native artifact makes is that a user who added a | |
| # dependency and installed nothing has an engine. Nothing in the test | |
| # suite can check that, because the suite is told where the library is | |
| # so that it tests the binding rather than the search. So it is | |
| # checked here, once, the way a user meets it: a classpath, no | |
| # property, no environment variable, and a statement. | |
| natives: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: tamnd/zu | |
| path: engine | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| cache: maven | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: engine | |
| - name: Build libzu | |
| working-directory: engine | |
| run: cargo build --release -p zu-capi | |
| # One platform rather than seven, because this runner can only | |
| # build the one it is, so the rule that every platform is staged | |
| # is stood down here and holds where it matters, in the release. | |
| - name: Stage the one platform this runner is | |
| run: | | |
| set -eu | |
| case "$RUNNER_OS" in | |
| Linux) flavour=linux-amd64; library=libzu.so ;; | |
| macOS) flavour=darwin-arm64; library=libzu.dylib ;; | |
| *) echo "no row for $RUNNER_OS"; exit 1 ;; | |
| esac | |
| mkdir -p "zudb-native/lib/$flavour" | |
| cp "engine/target/release/$library" "zudb-native/lib/$flavour/$library" | |
| - run: mvn $MAVEN_ARGS -Pnatives -DskipTests -Denforcer.skip=true package | |
| - name: A classpath, and nothing else | |
| run: | | |
| set -eu | |
| mkdir -p "$RUNNER_TEMP/user" | |
| cat > "$RUNNER_TEMP/user/Main.java" <<'EOF' | |
| import dev.zudb.Connection; | |
| import dev.zudb.Result; | |
| import dev.zudb.Zu; | |
| public class Main { | |
| public static void main(String[] args) { | |
| System.out.println("found " + Zu.library() + " through " + Zu.source()); | |
| try (Connection conn = Connection.memory(); | |
| Result r = conn.query("RETURN 1 AS one")) { | |
| if (r.row(0).getLong(0) != 1L) { | |
| throw new AssertionError("the engine answered something else"); | |
| } | |
| } | |
| System.out.println("the engine came out of the jar and answered"); | |
| } | |
| } | |
| EOF | |
| cp=$(ls zudb/target/zudb-*.jar zudb-ffm/target/zudb-ffm-*.jar \ | |
| zudb-native/target/zudb-native-*.jar | grep -v sources | tr '\n' ':') | |
| javac -cp "$cp" -d "$RUNNER_TEMP/user" "$RUNNER_TEMP/user/Main.java" | |
| env -u ZU_LIBRARY java --enable-native-access=ALL-UNNAMED \ | |
| -cp "$cp$RUNNER_TEMP/user" Main | |
| # An image has no linker in it. Every downcall stub is machine code | |
| # written while the image is built, from a file that says which | |
| # signatures to write, and a file that is wrong produces an image that | |
| # builds clean and dies on the first query. The unit test checks that | |
| # file against what the binding binds; only an image can check that the | |
| # file is the file the builder wanted, so one gets built here and run. | |
| native: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: tamnd/zu | |
| path: engine | |
| - uses: graalvm/setup-graalvm@v1 | |
| with: | |
| java-version: "25" | |
| distribution: graalvm | |
| cache: maven | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: engine | |
| - name: Build libzu | |
| working-directory: engine | |
| run: cargo build --release -p zu-capi | |
| - name: Stage the one platform this runner is | |
| run: | | |
| set -eu | |
| case "$RUNNER_OS" in | |
| Linux) flavour=linux-amd64; library=libzu.so ;; | |
| macOS) flavour=darwin-arm64; library=libzu.dylib ;; | |
| *) echo "no row for $RUNNER_OS"; exit 1 ;; | |
| esac | |
| mkdir -p "zudb-native/lib/$flavour" | |
| cp "engine/target/release/$library" "zudb-native/lib/$flavour/$library" | |
| - run: mvn $MAVEN_ARGS -Pnatives -DskipTests -Denforcer.skip=true package | |
| # The same program the natives job runs on a JVM, compiled to a | |
| # binary instead. No property, no environment variable, and the | |
| # library comes out of the image rather than off the disk. | |
| - name: An image, and nothing else | |
| run: | | |
| set -eu | |
| mkdir -p "$RUNNER_TEMP/user" | |
| cat > "$RUNNER_TEMP/user/Main.java" <<'EOF' | |
| import dev.zudb.Connection; | |
| import dev.zudb.Result; | |
| import dev.zudb.Zu; | |
| public class Main { | |
| public static void main(String[] args) { | |
| System.out.println("found " + Zu.library() + " through " + Zu.source()); | |
| try (Connection conn = Connection.memory(); | |
| Result r = conn.query("UNWIND [1, 2, 3] AS n RETURN sum(n) AS total")) { | |
| if (r.row(0).getLong(0) != 6L) { | |
| throw new AssertionError("the image answered something else"); | |
| } | |
| } | |
| System.out.println("the engine came out of the image and answered"); | |
| } | |
| } | |
| EOF | |
| cp=$(ls zudb/target/zudb-*.jar zudb-ffm/target/zudb-ffm-*.jar \ | |
| zudb-native/target/zudb-native-*.jar | grep -v sources | tr '\n' ':') | |
| javac -cp "$cp" -d "$RUNNER_TEMP/user" "$RUNNER_TEMP/user/Main.java" | |
| native-image -cp "$cp$RUNNER_TEMP/user" \ | |
| --no-fallback -o "$RUNNER_TEMP/user/main" Main | |
| env -u ZU_LIBRARY "$RUNNER_TEMP/user/main" | |
| # A binding holds native memory, and the process that finds out later | |
| # is the user's. The suite cannot see that: a test that closes nothing | |
| # and asserts on a message passes, and the memory it left behind is | |
| # somebody else's problem an hour into a run. | |
| # | |
| # So the allocator is asked instead. LeakSanitizer is loaded ahead of | |
| # the JVM, a driver opens and closes every handle this client hands | |
| # out, and the report is read for blocks whose stack names libzu. The | |
| # JVM's own unfreed megabyte is not read, because a JVM does not free | |
| # at exit on purpose and none of it is anything a user can act on. | |
| # | |
| # Both providers, because the two allocate down different paths: FFM | |
| # arenas on one side, NewDirectByteBuffer and a shim on the other, and | |
| # a leak in one of them is invisible from the other. | |
| leaks: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| provider: [ffm, jni] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: tamnd/zu | |
| path: engine | |
| # 25 for both rows. The FFM provider cannot be compiled by anything | |
| # older, and the shim asks for JNI 1.8 in JNI_OnLoad, so the JNI | |
| # row is the same code a caller on 17 runs. | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| cache: maven | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: engine | |
| # Not built with the sanitizer, deliberately. Interposing the | |
| # allocator is enough to see a block nobody freed, and an | |
| # instrumented engine would mean building the whole of Rust twice | |
| # to answer a question about this repository. | |
| - name: Build libzu | |
| working-directory: engine | |
| run: cargo build --release -p zu-capi | |
| - name: Where the library landed | |
| run: echo "ZU_LIBRARY=$GITHUB_WORKSPACE/engine/target/release/libzu.so" >> "$GITHUB_ENV" | |
| - name: Build the JNI shim | |
| if: matrix.provider == 'jni' | |
| run: ./scripts/build-shim.sh | |
| - run: ./scripts/leaks.sh ${{ matrix.provider }} | |
| # What Maven Central will run over the artifacts, run here instead so | |
| # that a release is not the first time anyone sees it. | |
| javadoc: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: "25" | |
| cache: maven | |
| - run: mvn $MAVEN_ARGS -P release -DskipTests package |