aho135 commented on code in PR #20314:
URL: https://github.com/apache/druid/pull/20314#discussion_r3984487462


##########
sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java:
##########
@@ -220,6 +221,10 @@ public SqlConformance conformance()
             if (aClass.equals(PlannerContext.class)) {
               return (C) plannerContext;
             }
+            if (aClass.equals(CancelFlag.class)) {

Review Comment:
   Fixed in fd86a12573. You're right that `Programs.of` builds its `HepPlanner` 
with a null `Context`, so the per-query `CancelFlag` never reached the 
pre/reduction/pre-Volcano/cleanup Hep stages (the reduction stage is where 
`RexSimplify`/CALCITE-3178 runs). I added `CalciteRulesManager#hepProgram`, 
which constructs the `HepPlanner` with 
`Contexts.of(plannerContext.getCancelFlag())` and is otherwise identical to 
`Programs.of(hepProgram, true, DefaultRelMetadataProvider.INSTANCE)`, and 
switched those stages to use it. The cost-based Volcano stages already inherit 
the flag via the cluster planner's context. 445 
`DecoupledPlanningCalciteQueryTest` cases pass unchanged, confirming the helper 
is behavior-preserving.



##########
sql/src/main/java/org/apache/druid/sql/DirectStatement.java:
##########
@@ -196,20 +197,44 @@ public ResultSet plan()
     }
     long planningStartNanos = System.nanoTime();
     try (DruidPlanner planner = createPlanner()) {
-      validate(planner);
-      authorize(planner, authorizer());
+      // Bound the wall-clock time spent planning this query. A non-positive 
timeout disables this.
+      final long maxPlanningTimeMs = 
planner.getPlannerContext().getPlannerConfig().getMaxPlanningTimeMs();
+      try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(
+          maxPlanningTimeMs,
+          planner.getPlannerContext().getCancelFlag(),

Review Comment:
   Fixed in fd86a12573. Since `TableMacro.apply()` gives no handle to the outer 
context, I added `PlannerContext#withInheritedCancelFlag`: 
`DirectStatement#plan()` installs the top-level query's `CancelFlag` on a 
thread-local for the duration of planning, and any `PlannerContext` created on 
that thread (including the nested planner in `DruidViewMacro`) inherits it 
instead of creating its own. So a single planning timeout now governs view 
expansion too. (Interrupt remains advisory, as you note, but the shared flag 
reaches the nested Volcano/Hep planners.)



##########
sql/src/main/java/org/apache/druid/sql/DirectStatement.java:
##########
@@ -196,20 +197,44 @@ public ResultSet plan()
     }
     long planningStartNanos = System.nanoTime();
     try (DruidPlanner planner = createPlanner()) {
-      validate(planner);
-      authorize(planner, authorizer());
+      // Bound the wall-clock time spent planning this query. A non-positive 
timeout disables this.
+      final long maxPlanningTimeMs = 
planner.getPlannerContext().getPlannerConfig().getMaxPlanningTimeMs();
+      try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(
+          maxPlanningTimeMs,
+          planner.getPlannerContext().getCancelFlag(),
+          Thread.currentThread()
+      )) {
+        try {
+          validate(planner);
+          authorize(planner, authorizer());
 
-      // Adding the statement to the lifecycle manager allows cancellation.
-      // Tests cancel during this call; real clients might do so if the plan
-      // or execution prep stages take too long for some unexpected reason.
-      sqlToolbox.sqlLifecycleManager.add(sqlQueryId(), this);
-      transition(State.PREPARED);
-      resultSet = createResultSet(createPlan(planner));
-      prepareResult = planner.prepareResult();
-      // Double check needed by SqlResourceTest
-      transition(State.PREPARED);
-      reporter.planningTimeNanos(System.nanoTime() - planningStartNanos);
-      return resultSet;
+          // Adding the statement to the lifecycle manager allows cancellation.
+          // Tests cancel during this call; real clients might do so if the 
plan
+          // or execution prep stages take too long for some unexpected reason.
+          sqlToolbox.sqlLifecycleManager.add(sqlQueryId(), this);
+          transition(State.PREPARED);
+          resultSet = createResultSet(createPlan(planner));
+          prepareResult = planner.prepareResult();
+          // Double check needed by SqlResourceTest
+          transition(State.PREPARED);
+          reporter.planningTimeNanos(System.nanoTime() - planningStartNanos);
+          return resultSet;

Review Comment:
   Fixed in fd86a12573. `plan()` now re-checks `timeout.isTimedOut()` on the 
success path (after `createPlan`/`prepareResult` return normally), not just in 
the catch, and throws `QueryTimeoutException` if the watchdog had already fired 
— so a plan completed in a non-cancellable section that swallowed the interrupt 
is rejected rather than executed.



##########
sql/src/main/java/org/apache/druid/sql/DirectStatement.java:
##########
@@ -196,20 +197,44 @@ public ResultSet plan()
     }
     long planningStartNanos = System.nanoTime();
     try (DruidPlanner planner = createPlanner()) {
-      validate(planner);
-      authorize(planner, authorizer());
+      // Bound the wall-clock time spent planning this query. A non-positive 
timeout disables this.
+      final long maxPlanningTimeMs = 
planner.getPlannerContext().getPlannerConfig().getMaxPlanningTimeMs();
+      try (SqlPlanningTimeout timeout = SqlPlanningTimeout.arm(

Review Comment:
   Fixed in fd86a12573. The watchdog is now armed with the remaining budget 
measured from `planningStartNanos` (`remainingPlanningBudgetMs` = 
`maxPlanningTimeMs` minus the time already elapsed, floored at 1ms), so 
planner/schema construction counts against the budget and a query can't get a 
fresh full timeout after an expensive setup.



##########
sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java:
##########
@@ -205,6 +234,7 @@ public boolean equals(Object o)
            && useNativeQueryExplain == that.useNativeQueryExplain
            && forceExpressionVirtualColumns == 
that.forceExpressionVirtualColumns
            && maxNumericInFilters == that.maxNumericInFilters
+           && maxPlanningTimeMs == that.maxPlanningTimeMs

Review Comment:
   Good catch, fixed in fd86a12573. `getNonDefaultAsQueryContext()` now emits 
`maxPlanningTimeMs` when non-default, so the defensive config<->context 
equality check passes; added 
`testMaxPlanningTimeMsRoundTripsThroughQueryContext` to `PlannerConfigTest` to 
cover it.



-- 
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]

Reply via email to