Skip to content

Two things the corpus runner got wrong about the engine #40

Two things the corpus runner got wrong about the engine

Two things the corpus runner got wrong about the engine #40

Workflow file for this run

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