yashmayya opened a new pull request, #19102:
URL: https://github.com/apache/pinot/pull/19102
## What this closes
Six modules deliberately compile to Java 11 bytecode with a hard-coded
compiler `release` (not inherited from `${jdk.version}`), so that third-party
plugins built against `pinot-spi` and applications embedding the Java/JDBC
client are not forced onto JDK 25:
`pinot-spi`, `pinot-segment-spi`, `pinot-timeseries/pinot-timeseries-spi`,
`pinot-common`, `pinot-clients/pinot-java-client`,
`pinot-clients/pinot-jdbc-client`.
`--release 11` already guarantees Pinot's **own** code in those modules is
Java-11-clean — it restricts against the Java 11 API signature set, not just
the bytecode version. **The gap is the transitive dependency closure, which
nothing verifies.** Today it happens to hold (arrow-vector 19.0.0 is major 55,
calcite-core 1.42.0 is major 52, helix-core 2.0.1 is major 55), but any routine
dependency bump could ship a Java 17+ jar into the client's closure and nobody
would notice until a user on Java 11 reports `UnsupportedClassVersionError` or
`NoSuchMethodError`. Every existing matrix in `.github/workflows/` is `java: [
25 ]`; no job anywhere loads these artifacts on a Java 11 JVM.
A job that only inspected class-file major versions of Pinot's own jars
would be vacuous, so this one **executes** the clients on a real Java 11 JVM.
## Shape
The build cannot run on Java 11 — the root pom enforces `requireJavaVersion
[25,)`, and Pinot's services have a genuine Java 25 floor (`datasketches-java`
9.0.0 is major 69 and the code uses `java.lang.foreign.MemorySegment`, see
28922369c8 / #19014). `-Djdk.version=11` does not help; that knob only lowers
the compile target while the enforcer checks the *running* JDK. So the job is
necessarily two-JDK: **build with JDK 25, then run the verification under Java
11.**
- `.github/workflows/pinot_java11_client_compatibility.yml` — installs Java
11 first and JDK 25 second (so `JAVA_HOME` is the build JDK), and passes the
Java 11 path via `steps.java11.outputs.path`. I confirmed `path` is a real
output of `actions/setup-java@v5` rather than assuming it; the script
additionally falls back to `JAVA_HOME_11_X64` / `JAVA_HOME_11_ARM64` so it does
not depend on the runner architecture.
- `.github/workflows/scripts/.pinot_java11_client_compat.sh` — resolves the
Java 11 JVM, builds `-pl pinot-java11-client-verifier -am`, then launches the
verifier with the resolved runtime closure.
- `pinot-java11-client-verifier/` — a new module at the reactor root,
alongside the existing `pinot-compatibility-verifier` and
`pinot-dependency-verifier` (both of which are root modules, and the former has
exactly this shape: a runnable `src/main/java` verifier driven by a script in
`.github/workflows/scripts/`). Compiled at `release 11` itself, since the
script launches its classes on the Java 11 JVM. `maven.deploy.skip` keeps a CI
harness out of the published artifacts, and nothing in the reactor depends on
it.
The module depends on both clients, which makes its resolved runtime closure
the union of the two consumer-facing closures.
`maven-dependency-plugin:build-classpath` writes that closure to
`target/runtime-classpath.txt` at `prepare-package`, and the script hands
exactly that to `java -cp`.
## What the 18 checks do
**Closure scan** (`ClasspathClosureScanner`) — walks all ~220 classpath
entries and fails on any class file a Java 11 JVM could not load. This is a
legitimate *complement* to `--release`, not a duplicate: it covers third-party
jars, which `--release` says nothing about. Two categories are correctly not
violations, because a Java 11 JVM never loads them:
- `module-info.class` descriptors, at any version.
- `META-INF/versions/<n>/` entries where `n` > the target release.
The multi-release handling is load-bearing, not defensive:
**`jackson-core-2.22.1` ships major 61 under `META-INF/versions/17/` and major
65 under `versions/21/`, and `jersey-common-2.48` ships major 65** — without it
the job would fail on a dependency every Pinot consumer has.
**Runtime exercises** — these construct and use the artifacts, pulling in
Jackson, Arrow, Calcite, Helix, protobuf, gRPC/Netty, async-http-client and the
JNI compression codecs:
| Check | What it pulls in |
|---|---|
| `spi-schema-deserialization`, `spi-table-config-deserialization` |
pinot-spi + Jackson |
| `segment-spi-data-buffer` | off-heap `PinotDataBuffer` — `Unsafe` /
`sun.misc.Cleaner` reflection, which the class-file scan cannot see |
| `timeseries-spi-plan-serde` | `TimeSeriesPlanSerde` round trip,
`TimeBuckets` |
| `common-data-schema-round-trip`, `common-broker-response-deserialization`
| `DataSchema` binary + JSON, `BrokerResponseNative` over a realistic broker
response |
| `common-response-encoders` | JSON **and Arrow** encode/decode —
arrow-vector + arrow-memory-netty are the likeliest source of a floor bump, and
they only fail when actually exercised |
| `common-calcite-sql-parsing` | `CalciteSqlParser` on a
group-by/order-by/limit query plus a `SET` option |
| `common-helix-segment-metadata` | `SegmentZKMetadata` ↔ `ZNRecord` via
`ZNRecordSerializer`, `ExternalView` |
| `common-grpc-response-decoding` | the gRPC decode path end to end:
protobuf `BrokerResponse` → every compression codec → every encoder, exactly as
`GrpcConnection` unpacks a server response |
| `common-grpc-channel-construction` | `BrokerGrpcQueryClient` → Netty
channel + pooled direct-buffer allocator |
| `java-client-http-transport` | async-http-client / Netty transport
construction |
| `java-client-query-execution`, `java-client-prepared-statement` |
`ConnectionFactory` → `Connection.execute` → `ResultSetGroup` values +
`ExecutionStats`, against a canned transport |
| `jdbc-driver-registration` | `DriverManager` auto-discovery through
`META-INF/services/java.sql.Driver` and `ServiceLoader`, with no explicit
`Class.forName` |
| `jdbc-result-set` | `PinotResultSet` rows, `ResultSetMetaData`, SQL type
mapping |
No live cluster, per the tradeoff below.
**Vacuity guards.** A green job that cannot go red is worse than no job, so
three things are asserted before any of the above counts:
- the JVM really is Java 11 (`Runtime.version().feature()`), because running
on the build JDK would make every other check pass for the wrong reason.
Failing this aborts the run rather than printing 17 meaningless passes.
- at least 100 jars and 20,000 class files were inspected **inside
archives**, counted separately from directory entries — otherwise the
verifier's own `target/classes` would satisfy the guard on its own and make it
unfalsifiable.
- all six Java-11-pinned modules are present on the scanned closure, so
marking one `optional`/`provided` shrinks coverage loudly instead of silently.
## Proving it can fail
Run locally against Temurin 11.0.22. Baseline: 18/18 pass, exit 0, `major
versions: {46=236, 47=1778, 48=104, 49=3104, 50=4391, 51=1612, 52=52314, 53=15,
55=5926}` — no bucket above 55, and the counts make it evident the scan really
looked.
**1. Third-party dependency regression (the actual threat).** Added
`org.apache.datasketches:datasketches-java` — already at 9.0.0 in
`dependencyManagement`, and major 69 precisely because #19014 moved it to Java
25 — to `pinot-java-client`. This is the realistic shape: someone adds a
sketch-backed client helper and silently breaks every Java 11 consumer.
```
scanned 219 archives (69913 class files) ... major versions: {..., 55=5926,
69=440}
FAIL classpath-closure-is-loadable
440 class file(s) on the client runtime closure cannot be loaded by
Java 11. A dependency was
bumped to a release that no longer supports Java 11; pin it back or
drop it from the client
closure. Offenders:
.../datasketches-java-9.0.0.jar!/org/apache/datasketches/common/ArrayOfBooleansSerDe.class
(class file major version 69, needs Java 25)
... and 400 more
```
Script exit 1.
**2. Post-Java-11 API with the pin in place.** Added
`Arrays.stream(_columnNames).toList()` (Java 16+) to `DataSchema.toString()`.
`--release 11` rejects it at **compile** time:
```
DataSchema.java:[226,73] cannot find symbol
symbol: method toList()
location: interface Stream<String>
```
This is the pre-existing guarantee, and it is exactly why the closure scan
is a complement rather than a duplicate — this failure mode cannot reach
runtime while the pin is in place.
**3. Pin removed — proving the *execution* checks go red, not just the
scan.** Kept the `toList()` call and changed `pinot-common` to `release 17`. It
now compiles, and pinot-common ships major 61:
```
scanned 218 archives ... major versions: {..., 55=4738, 61=1188}
FAIL classpath-closure-is-loadable 1188 class file(s) ... cannot be
loaded by Java 11
FAIL common-data-schema-round-trip
java.lang.UnsupportedClassVersionError:
org/apache/pinot/common/utils/DataSchema has been
compiled by a more recent version of the Java Runtime (class file
version 61.0), this version
of the Java Runtime only recognizes class file versions up to 55.0
FAIL common-broker-response-deserialization ... BrokerResponseNative
... 61.0
FAIL common-response-encoders ... ResultTable ... 61.0
FAIL common-calcite-sql-parsing ... CalciteSqlParser ...
61.0
FAIL common-helix-segment-metadata ... SegmentZKMetadata ...
61.0
FAIL common-grpc-response-decoding ... ResultTable ... 61.0
FAIL common-grpc-channel-construction ... BrokerGrpcQueryClient
... 61.0
FAIL java-client-query-execution ...
```
Eight exercise checks fail by name, so the harness catches a real load
failure and not merely a jar inspection.
**4. Mis-wired JDK.** Same classpath on JDK 25:
```
FAIL jvm-is-at-target-feature-version
expected to be running on a Java 11 JVM but this is Java 25 (...);
verifying the clients on
the build JDK would make every other check vacuous
Aborting: the remaining 17 checks would not tell us anything on this JVM.
```
Exit 1. (Arrow also fails on 25 without `--add-opens`, which is consistent
with the deliberate absence of those flags — see below.)
**5. Coverage silently shrinking.** Swapped the `pinot-timeseries-spi` jar
for an identically-contented file under a different name, so its classes still
load but the artifact is unrecognisable:
```
FAIL classpath-closure-is-loadable
these Java 11 pinned modules are not on the verified closure, so
this job is no longer
checking them: [pinot-timeseries-spi]. Either restore the dependency
or update
JAVA11_PINNED_MODULES.
```
All injections reverted; the tree is clean and the baseline run is green
again.
`ClasspathClosureScannerTest` (14 tests, runs in the normal unit-test job —
no Java 11 JVM needed) pins the scanner's fail path, since both of its filters
fail *open* and a regression there would produce a permanently green job rather
than a red one. It covers: major 65 at the jar root → violation;
`module-info.class` at 65 → ignored; `META-INF/versions/17|21/` above target →
ignored; `versions/9/` at or below target → violation; malformed version
directory → ignored; missing `0xCAFEBABE` and truncated entries → not counted;
archive vs directory counters kept apart; corrupt zip → `IOException`;
reporting capped while the total count stays exact; target version honoured at
11/17/21.
## Notes on two deliberate choices
**No live cluster.** A cross-JVM integration test (cluster on JDK 25, client
on Java 11) would be higher fidelity, but much slower and flakier, and it would
put a multi-process cluster on the critical path of what should be a fast
signal on every PR. The canned-transport approach still drives the real
`Connection` / `ResultSetGroup` / `PinotResultSet` code and the real
deserialization paths, which is where a bad bump surfaces. If someone wants the
full-fidelity version later it belongs in a separate opt-in job, not as a
dependency of this smoke.
**No `--add-opens` / `-Dio.netty.tryReflectionSetAccessible=true`.** Those
flags exist in Pinot's launch scripts for JDK 17+; adding them here would paper
over the runtime breakage this job exists to catch. Verified empirically that
nothing on the closure needs them on Java 11 — Arrow logs an
illegal-reflective-access warning and works.
`-Dshade.phase.prop=none` on the build is worth a note: `pinot-java-client`
and `pinot-jdbc-client` each produce a ~148 MB shaded jar, and
`shadedArtifactAttached=true` means those jars never appear on the classpath
this job verifies. Without the flag the job spends minutes building 296 MB it
does not use and then pushes it into `~/.m2`, which `actions/cache` uploads
under a key the other Maven jobs share. `-DskipShade=true` is **not**
sufficient — it only deactivates the `pinot-jdbc-client` profile, while
`pinot-java-client` sets `shade.phase.prop=package` unconditionally. Both
verified by inspecting the produced jars.
## Follow-ups, deliberately not in this PR
Found while analysing this area; left out to keep the change to one concern:
- `.github/workflows/build-pinot-base-docker-image.yml:32` and `:63` still
build base images for `jdk_version: [21, 25]`. `pinot-base-build:21-*` can no
longer build master (the enforcer rejects Java 21) and
`pinot-base-runtime:21-*` would hit the datasketches Java 25 floor at runtime.
`docker/images/pinot-base/pinot-base-build/*.dockerfile` and
`.../pinot-base-runtime/*.dockerfile` also still default `ARG JAVA_VERSION=21`.
`workflow_dispatch`-only, so latent rather than actively broken.
- `pinot-tools/src/main/resources/appAssemblerScriptTemplate:204` guards the
`--add-opens` flags behind `if [ "$(jdk_version)" -gt 11 ]`, which can never be
false now.
- The closure scan is the one check that does not need a Java 11 JVM, so it
could alternatively live in `extra-enforcer-rules`' `enforceBytecodeVersion`
and fire on every `./mvnw install`. That is a project-wide dependency-policy
decision, and keeping it next to the exercise checks means one place reports on
the whole closure.
## Release notes
`none`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]