Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 55 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,44 @@ jobs:
!core/target/*-sources.jar
if-no-files-found: error

# The 1.3.5-1.3.7 releases shipped a JDK 8-built jar whose FdBig class links
# sun.misc.FDBigInteger (gone since Java 9) and whose module name degraded to
# the filename-derived "questdb.client". Both are only observable in the
# PACKAGED jar on a modern JDK, so take the jar built by build-jdk8 and prove
# on JDK 25 that (a) the automatic module name is io.questdb.client and
# (b) slow-path double formatting resolves the META-INF/versions/11 bridge.
mrjar-smoke-jdk25:
name: MRJAR smoke (JDK 8 jar on JDK 25)
# Takes the JDK 8-built jar (the shipped artifact) from build-jdk8 and runs it
# on every newer JDK the client must work on, asserting (a) the automatic
# module name is io.questdb.client and (b) slow-path double formatting
# resolves the META-INF/versions/11 bridge. These are only observable in the
# PACKAGED jar on a modern JDK, and each regression here was invisible to the
# other jobs until the jar was EXECUTED on the right JDK/flags:
# * 1.3.5-1.3.7: FdBig linked sun.misc.FDBigInteger (gone since Java 9) and
# the module name degraded to the filename-derived "questdb.client".
# * issue #96: JDK 26 made FDBigInteger package-private (passed 8/11/25).
# * JDK 27 compact object headers (JEP 450, default-on) shifted the
# AccessibleObject.override offset the bridge relies on -- guarded by the
# per-layout runs in the smoke step below, blocking on the 25/26 legs.
# The 27-ea entry is a forward canary: it should be green, and does not fail
# the workflow because EA is a moving target -- a red run means the next JDK
# needs attention.
mrjar-smoke:
name: MRJAR smoke (JDK 8 jar on JDK ${{ matrix.java }})
needs: build-jdk8
runs-on: ubuntu-latest
timeout-minutes: 15
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
java: ["25", "26"]
experimental: [false]
include:
- java: "27-ea"
experimental: true
steps:
- name: Check out
uses: actions/checkout@v4

- name: Set up JDK 25
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "25"
java-version: ${{ matrix.java }}

- name: Download JDK 8-built client jar
uses: actions/download-artifact@v4
Expand All @@ -120,26 +138,46 @@ jobs:
grep -q '^io\.questdb\.client@' module.txt
javac -cp "$jar_file" -d smoke-classes \
core/src/test/java/io/questdb/client/test/std/DoubleFormatSmoke.java
java -cp "$jar_file:smoke-classes" io.questdb.client.test.std.DoubleFormatSmoke
# The FdBig double formatter reaches jdk.internal.math.FDBigInteger by
# setting AccessibleObject.override at an offset Unsafe derives from the
# object header size. Run the smoke under each object-header layout so a
# regression to a hard-coded offset fails a BLOCKING leg (25, 26), not
# only the non-blocking 27-ea canary. Compact object headers (JEP 450)
# are a product flag on JDK 25/26 and default-on from 27; the
# +IgnoreUnrecognizedVMOptions guard keeps the flags harmless on any JDK.
run_smoke() { # <label> <jvm flags...>
local label="$1"; shift
echo "::group::DoubleFormatSmoke [$label]"
java "$@" -cp "$jar_file:smoke-classes" io.questdb.client.test.std.DoubleFormatSmoke
echo "::endgroup::"
}
run_smoke "default" -XX:+IgnoreUnrecognizedVMOptions
run_smoke "compact-headers" -XX:+IgnoreUnrecognizedVMOptions -XX:+UseCompactObjectHeaders
run_smoke "uncompressed-oops" -XX:+IgnoreUnrecognizedVMOptions -XX:-UseCompressedOops -XX:-UseCompressedClassPointers

# The client is also consumed as a submodule of the main questdb repo, which
# builds on JDK 25. Guard against JDK 25 compile breakage (main + test
# sources, both modules) and confirm the javadoc jar builds on JDK 25 too
# builds on JDK 25, and contributors build on the newest GA JDK (issue #96
# was "does not compile on JDK 26"). Guard against compile breakage on both
# (main + test sources, both modules) and confirm the javadoc jar builds too
# (-P javadoc attaches it at the package phase). Do NOT run the tests -- the
# parent repo runs them against a real server.
compile-jdk25:
name: Compile & javadoc smoke (JDK 25)
compile-modern-jdk:
name: Compile & javadoc smoke (JDK ${{ matrix.java }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
java: ["25", "26"]
steps:
- name: Check out
uses: actions/checkout@v4

- name: Set up JDK 25
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "25"
java-version: ${{ matrix.java }}
cache: maven

- name: Compile (main + test) and build javadoc (no tests run)
Expand Down
26 changes: 0 additions & 26 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@
<version>3.11.0</version>
<configuration>
<compilerArgs>
<arg>${compilerArg1}</arg>
<arg>${compilerArg2}</arg>
<arg>-J-XX:-TieredCompilation</arg>
<arg>-J-XX:TieredStopAtLevel=1</arg>
</compilerArgs>
Expand Down Expand Up @@ -266,10 +264,6 @@
<doclint>none</doclint>
<source>${javac.compile.source}</source>
<detectJavaApiLink>false</detectJavaApiLink>
<additionalJOptions>
<additionalJOption>${javadocJOption1}</additionalJOption>
<additionalJOption>${javadocJOption2}</additionalJOption>
</additionalJOptions>
<sourceFileExcludes>
<sourceFileExclude>${excludePattern1}</sourceFileExclude>
<sourceFileExclude>module-info.java</sourceFileExclude>
Expand Down Expand Up @@ -398,10 +392,6 @@
<doclint>none</doclint>
<source>${javac.compile.source}</source>
<detectJavaApiLink>false</detectJavaApiLink>
<additionalJOptions>
<additionalJOption>${javadocJOption1}</additionalJOption>
<additionalJOption>${javadocJOption2}</additionalJOption>
</additionalJOptions>
<sourceFileExcludes>
<sourceFileExclude>${excludePattern1}</sourceFileExclude>
<sourceFileExclude>module-info.java</sourceFileExclude>
Expand Down Expand Up @@ -524,13 +514,6 @@
<jdk.version>11</jdk.version>
<java.enforce.version>11</java.enforce.version>
<questdb.artifactid>questdb</questdb.artifactid>
<compilerArg1>--add-exports</compilerArg1>
<compilerArg2>java.base/jdk.internal.math=io.questdb.client</compilerArg2>
<!-- javadoc needs the same module export to resolve jdk.internal.math.FDBigInteger
referenced by the src/main/java11 FdBig bridge. Kept separate from the javac
compilerArgs because javadoc and javac accept different option sets. -->
<javadocJOption1>--add-exports</javadocJOption1>
<javadocJOption2>java.base/jdk.internal.math=io.questdb.client</javadocJOption2>
<excludePattern1>nothing-to-exclude-dummy-value-include-all-java11plus</excludePattern1>
<excludeTestPattern1>nothing-to-exclude-dummy-value-include-all-java11plus</excludeTestPattern1>
<javac.compile.source>${javac.target}</javac.compile.source>
Expand Down Expand Up @@ -559,13 +542,6 @@
<jdk.version>8</jdk.version>
<java.enforce.version>[1.8,11)</java.enforce.version>
<questdb.artifactid>questdb</questdb.artifactid>
<!-- JDK8 javac rejects the add-exports flag; use a harmless no-op flag instead -->
<compilerArg1>-Xlint:none</compilerArg1>
<compilerArg2>-Xlint:none</compilerArg2>
<!-- JDK8 javadoc rejects javac's -Xlint and needs no module export (the java8 FdBig
bridge uses the open sun.misc.FDBigInteger). Pass a harmless no-op instead. -->
<javadocJOption1>-quiet</javadocJOption1>
<javadocJOption2>-quiet</javadocJOption2>
<!-- module-info.java cannot be compiled on JDK8; exclude it from both the main
(<excludes> = excludeTestPattern1) and test (<testExcludes> = excludePattern1)
source sets. -->
Expand Down Expand Up @@ -621,8 +597,6 @@
the jar is broken on Java 9+ again (JarPackagingIT
cross-checks the packaged jar against the source root) -->
<javac srcdir="${project.basedir}/src/main/java11" destdir="${project.build.outputDirectory}/META-INF/versions/11" classpath="${project.build.outputDirectory}" source="11" target="11" encoding="UTF-8" debug="true" includeantruntime="false" fork="true" executable="${java11.home}/bin/javac" compiler="extJavac">
<compilerarg value="--add-exports" />
<compilerarg value="java.base/jdk.internal.math=ALL-UNNAMED" />
</javac>
</target>
</configuration>
Expand Down
48 changes: 16 additions & 32 deletions core/src/main/java/io/questdb/client/std/Unsafe.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,43 +202,27 @@ private static long AccessibleObject_override_fieldOffset() {
if (isJava8Or11()) {
return getFieldOffset(AccessibleObject.class, "override");
}
// From Java 12 onwards, AccessibleObject#override is protected and cannot be accessed reflectively.
boolean is32BitJVM = is32BitJVM();
if (is32BitJVM) {
return 8L;
}
if (getOrdinaryObjectPointersCompressionStatus(is32BitJVM)) {
return 12L;
}
return 16L;
// From Java 12 onwards, AccessibleObject#override is filtered from
// reflection, so its offset cannot be read directly. It is laid out at
// the first-field boundary -- immediately after the object header -- so
// the offset of the first (and only) field of a minimal probe class is
// identical. Measure it rather than hard-coding a value: JDK 24+ compact
// object headers (JEP 450, enabled by default in JDK 27) shrink the
// header from 12 to 8 bytes, and a hard-coded 12/16 would then point
// inside the header -- the Unsafe write to `override` would silently
// miss and setAccessible() would have no effect (surfacing as
// IllegalAccessError from the FdBig double-formatting bridge on JDK 27).
// The probe tracks compact (8), compressed (12), uncompressed (16) and
// 32-bit (8) layouts automatically.
return firstFieldBoundaryOffset();
}

private static boolean getOrdinaryObjectPointersCompressionStatus(boolean is32BitJVM) {
private static long firstFieldBoundaryOffset() {
class Probe {
@SuppressWarnings("unused")
private int intField; // Accessed through reflection

boolean probe() {
long offset = getFieldOffset(Probe.class, "intField");
if (offset == 8L) {
assert is32BitJVM;
return false;
}
if (offset == 12L) {
return true;
}
if (offset == 16L) {
return false;
}
throw new AssertionError(offset);
}
int intField; // read reflectively; sits at the object's first-field boundary
}
return new Probe().probe();
}

private static boolean is32BitJVM() {
String sunArchDataModel = System.getProperty("sun.arch.data.model");
return sunArchDataModel.equals("32");
return getFieldOffset(Probe.class, "intField");
}

private static boolean isJava8Or11() {
Expand Down
19 changes: 0 additions & 19 deletions core/src/main/java11/io/questdb/client/std/Compat.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

package io.questdb.client.std;

import java.lang.reflect.Method;

/**
* JDK-version-specific helpers. This is the Java 9+ variant; the parallel copy
* under {@code src/main/java8} provides Java 8 implementations of the same API.
Expand Down Expand Up @@ -54,21 +52,4 @@ public static long currentPid() {
public static void onSpinWait() {
Thread.onSpinWait();
}

/**
* Opens {@code java.base/jdk.internal.math} to this module so that
* {@code FDBigInteger} is reachable at runtime, mirroring the
* {@code --add-exports} flag used at compile time.
*/
static void exportFdBigInteger() {
try {
Module base = System.class.getModule();
Module current = Compat.class.getModule();
Method implAddExports = Module.class.getDeclaredMethod("implAddExports", String.class, Module.class);
Unsafe.makeAccessible(implAddExports);
implAddExports.invoke(base, "jdk.internal.math", current);
} catch (ReflectiveOperationException e) {
e.printStackTrace(System.out);
}
}
}
Loading
Loading