feat(apt): detect circular Q-class references and warn at compile time#1739
feat(apt): detect circular Q-class references and warn at compile time#1739o54711254 wants to merge 1 commit into
Conversation
Adds compile-time cycle detection in AbstractQuerydslProcessor using DFS traversal over entity type relationships. Emits a WARNING when circular static field references are found between Q-classes, which can cause class initialization deadlock in multi-threaded environments. - Only flags direct entity references (not collections) to avoid false positives - Only runs when createDefaultVariable is true - Reports all detected cycles with actionable remediation steps
|
Nice work on this — that deadlock has been a quiet menace for over a decade and a compile-time warning is the right escape hatch. 👍 A bit of cross-PR context: I'm working on #1728 which reshapes the KSP-generated Q-classes ( The deadlock is structurally absent on the KSP side, pinned by two dynamic tests:
So this detector would never need to fire for KSP-generated output. Whether that matters for your PR is up to the maintainer — just figured it's worth flagging in case the warning machinery ever lands somewhere that processes both pipelines. And for anyone reading this PR who is on Kotlin (or considering it): if your stack allows, switching codegen to |
|
@soidisant Thanks for the context and kind words! Good to know that KSP handles this structurally via Looking forward to seeing #1728 land! |
Problem Description
When two JPA entities have a bidirectional association (e.g.,
@OneToOne), QueryDSL APT generates Q-classes that contain circular static field references. If two threads simultaneously access these Q-classes for the first time, a class initialization deadlock occurs — all worker threads hang indefinitely without any exceptions or error logs.This issue has been a long-standing pain point since 2012 (querydsl/querydsl#146) and 2018 (querydsl/querydsl#2334). While it is mentioned as a known risk in the documentation, developers often have no way to detect it until their application hangs in production.
Proposed Solution
I have added a compile-time detection mechanism in
AbstractQuerydslProcessorto identify potential deadlocks before they reach runtime.Key Features:
globalVisitedset to ensure O(V+E) time complexity, preventing significant build-time slowdowns even in large projects.LIST,SET,MAP) because they are generated asListPath, etc., which do not trigger the target Q-class loading during the<clinit>phase.createDefaultVariableistrue, as disabling this option effectively prevents the deadlock.Example Warning Output:

The screenshot above demonstrates how the warning is presented during the Gradle build process.
Testing
Added comprehensive test cases in
QuerydslAnnotationProcessorCompileTestusinggoogle/compile-testing:circularQClassReference_producesWarning: Validates detection of direct bidirectional cycles.indirectCircularReference_producesWarning: Validates detection of multi-level cycles (A → B → C → A).unidirectionalReference_noWarning: Ensures no false warnings for safe, unidirectional paths.collectionReference_noWarning: Crucial Test - Confirms that@OneToMany(collections) do not trigger warnings, as they are safe from<clinit>deadlocks.Why This Location in
processAnnotations()The detection runs just before
context.clean(), aftertypeFactory.extendTypes(). At this point,context.entityTypesis fully populated with all entity properties, ensuring accurate cycle detection. Running it earlier would risk missing properties that haven't been resolved yet.