OpenFeign / OpenFeign/querydsl
Circular Q-class detection misses cycles passing through a `createDefaultVariable=false` entity
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 650
- Forks
- 102
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 47
Description
- I am willing to put in the work and submit a PR to resolve this issue.
Describe the bug
Follow-up to @velo's review on #1905.
AbstractQuerydslProcessor.detectCircularQClassReferences pre-filters context.entityTypes by createDefaultVariable() before handing the map to QClassCycleDetector. Because the detector resolves neighbors through that same map, a filtered entity disappears as both a node and its edges.
But edges are config-independent. createDefaultVariable gates only introDefaultInstance, which emits the public static final QOrder order = new QOrder("order") field. initEntityField emits new QC(forProperty("c"), inits.get("c")) into every constructor unconditionally.
So a cycle whose path runs through a createDefaultVariable=false entity is silently missed, even though the remaining entities still emit static default instances and can deadlock at runtime.
To Reproduce
Cycle A → B → C → A, with the default variable disabled on B only:
@QueryEntity
class A { B b; }
@QueryEntity
@Config(createDefaultVariable = false)
class B { C c; }
@QueryEntity
class C { A a; }
QA and QC still get static default instances; QB does not. The detector reports nothing, because dropping B from the map also deleted the A → B and B → C edges — but QB's constructor still bridges QA and QC at runtime.
Expected behavior
A cycle should be reported whenever runtime deadlock is possible, i.e. whenever ≥2 nodes on the cycle emit a static default instance. Intermediate createDefaultVariable=false entities should not exclude the cycle from detection.
Root cause
AbstractQuerydslProcessor:
private void detectCircularQClassReferences() {
Map<String, EntityType> entitiesWithDefaultVariable = new HashMap<>();
for (var entry : context.entityTypes.entrySet()) {
if (conf.getSerializerConfig(entry.getValue()).createDefaultVariable()) {
entitiesWithDefaultVariable.put(entry.getKey(), entry.getValue());
}
}
List<List<String>> detectedCycles =
QClassCycleDetector.detect(entitiesWithDefaultVariable);
...
}
QClassCycleDetector looks neighbors up in the map it is given, so filtering nodes up front also removes their edges. Node emission and edge emission are gated differently, so the two filters are not equivalent.
Proposed fix
- Run
QClassCycleDetector.detectover the full, unfilteredcontext.entityTypesso every real edge is traversed. - Report a detected cycle only when ≥2 of its nodes have
createDefaultVariable() == true.
≥2rather than≥1, per @velo's reasoning on #1905: with a single static instance the re-entry happens on the same thread, and JVM class initialization is re-entrant, so no deadlock is possible.
Test plan
Unit tests on QClassCycleDetector with plain EntityType fixtures:
A → B → C → A, default variable disabled onBonly → cycle reported.A ↔ Bwith it disabled on both → nothing reported.- Existing "no cycle" and "simple bidirectional cycle" cases unchanged.
Compile tests in querydsl-apt:
- Bidirectional pair compiled with
-Aquerydsl.createDefaultVariable=false→ no warning. This is the only coverage the per-entity filter would have; nothing exercises it today.
Separately, the existing compile tests assert only the header string, so the flag-naming line added in #1905 is untested. One line pins it:
.hadWarningContaining("-Aquerydsl.createDefaultVariable=false");
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in AbstractQuerydslProcessor.detectCircularQClassReferences and QClassCycleDetector, then read the existing unit tests and plain EntityType fixtures. Run the querydsl-apt tests; add coverage for a three-node cycle with one disabled default variable, a pair with both disabled, and the existing cycle cases. Done means cycles are reported when at least two nodes emit static default instances, and the compile test checks the -Aquerydsl.createDefaultVariable=false warning text.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- compilers, devtools, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100