ID |
|
|---|---|
Status |
Backlog |
Bucket |
bug |
Priority |
3 |
Theme |
testing |
Created |
2026-07-27 |
Updated |
2026-07-27 |
Pin around the Quarkus ArC unused-bean removal flake
The
quarkus:buildgoal ongraphitron-sakila-exampleintermittently fails CI with aNullPointerExceptioninside Quarkus ArC’s unused-bean removal pass. It is not reproducible locally, it is not caused by anything in our bean graph, and it is not fixed by upgrading Quarkus. Because the goal runs in thebuildjob thatdocs-buildanddocs-deploydepend on, a hit takes the documentation site down with it: a green fix can land on trunk and never reachgraphitron.sikt.no. Disable the removal pass for this module so the code path cannot run.
Observed failure
One occurrence so far, on a trunk push whose diff was two .adoc files, one test class, and two
roadmap markdown files. All 655 tests in the module passed; the failure came afterward, in
quarkus:build:
Failed to execute goal io.quarkus:quarkus-maven-plugin:3.34.5:build (default)
on project graphitron-sakila-example: Failed to build quarkus application:
[error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception:
java.lang.NullPointerException: Cannot invoke
"io.quarkus.arc.processor.InjectionPointInfo.isProgrammaticLookup()"
because "injectionPoint" is null
at io.quarkus.arc.processor.UnusedBeans.findRemovableBeans(UnusedBeans.java:44)
at io.quarkus.arc.processor.BeanDeployment.removeUnusedBeans(BeanDeployment.java:501)
at io.quarkus.arc.processor.BeanDeployment.removeUnusedComponents(BeanDeployment.java:410)
at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:357)
A re-run of the same commit went green with no code change.
What was established
-
Not ours. Quarkus augmentation indexes main classes and dependencies, not
src/test/java. The module’s entire bean graph isSakilaGraphitronApplicationplusgraphitron-jakarta-rest’s `GraphqlResource; the generated sources carry zero CDI annotations, so generated output cannot contribute a bean or an injection point. The same graph had been green on every preceding trunk run. -
Not reproducible locally. Four green runs on the exact failing commit: three of
mvn package -pl :graphitron-sakila-example -Plocal-db -DskipTests, plus one ofmvn verify -Plocal-db --batch-mode -T 1C, which is byte-for-byte the CI command. CI’s contended runner is the only place it has appeared. -
Not fixed by a version bump.
UnusedBeans.javais byte-identical between 3.34.5 (ours) and 3.38.0 (latest at time of writing), verified by diffing theio.quarkus.arc:arc-processorsources jars.BeanDeployment.javadid change, but still allocatesinjectionPointsas aCopyOnWriteArrayListand still passes it straight toUnusedBeans.findRemovableBeans. Bumping Quarkus may be worth doing on its own merits; it will not address this. -
The dereference site is single-threaded.
UnusedBeans.findRemovableBeansiteratesinjectionPointsin a plainforloop with no null tolerance, so the null was already in the list when the method was called. The bug is in whoever added it, not in the loop.
Hypothesis, not yet proven
BeanDeployment.injectionPoints is a CopyOnWriteArrayList filled by repeated addAll(...)
calls from collections assembled across Quarkus’s build-step threads, including the synthetic
injection points contributed by bean-registration extensions.
CopyOnWriteArrayList.addAll(Collection) snapshots its argument through toArray(), and
toArray() on a plain ArrayList being mutated concurrently can return an array with trailing
nulls, which the COW list then stores verbatim. That would produce a null with no offending bean
to point at, invisible on an idle machine, surfacing under load. Consistent with every symptom,
but confirming it needs the failure captured under a debugger, which a green environment cannot
provide. The pin below does not depend on the hypothesis being right.
Proposed fix
Set quarkus.arc.remove-unused-beans=false in
graphitron-sakila-example/src/main/resources/application.properties.
BeanDeployment.init guards the whole removal pass behind if (removeUnusedBeans), and that
field is the builder-level reflection of this property, so with it off removeUnusedComponents
never runs, findRemovableBeans is never called, and the null cannot be dereferenced. Dead-bean
removal is a production startup-footprint optimisation; this module is an example app whose
purpose is to compile emitted sources at release 17 and exercise them against a real database, so
it gains nothing from the pass.
Carry a comment naming the mechanism and the reason, so a later reader does not delete the property as unexplained cruft.
Open questions for the Spec pass
-
Property or POM? The property is closer to where a reader looks; a
<quarkus.arc.remove-unused-beans>system property in the module’s plugin configuration keeps it out of the runtime config that the tutorial narrates. Pick one and say why. -
Does anything else in the reactor run
quarkus:build? If a second module ever does, the pin belongs somewhere shared rather than duplicated. -
Worth reporting upstream? The COW-plus-
toArraymechanism above is a genuine defect if confirmed, and a report costs little. Nobody has searched the Quarkus tracker yet; GitHub access in the session that found this was scoped tosikt-no/graphitron.
Out of scope
-
Bumping Quarkus. Established above as not a fix for this. If a bump is wanted, it is its own item with its own regression surface.
-
Hardening our own code against the null. The list is Quarkus-internal; we have no seam.
-
Any change to the CI job layout. Making
docs-deployindependent ofbuildwould decouple the documentation site from unrelated module failures, which is arguably worth doing, but it is a separate question from this flake and should not ride along.