This is an automated email from the ASF dual-hosted git repository.
924060929 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 434e8563aae [fix](fe) Allocate a fresh StatementContext per EXECUTE to
prevent FE OOM in long-lived prepared statements (#67256)
434e8563aae is described below
commit 434e8563aaed8dfe45fea646aad2612021745a07
Author: starocean999 <[email protected]>
AuthorDate: Fri Sep 11 16:05:19 2026 +0800
[fix](fe) Allocate a fresh StatementContext per EXECUTE to prevent FE OOM
in long-lived prepared statements (#67256)
Problem Summary:
A prepared statement lives as long as its connection. The
`PreparedStatementContext` kept in
`ConnectContext.preparedStatementContextMap` retains a single
`StatementContext` and reuses the same object across every `EXECUTE` for
the whole connection lifetime.
Because one object is reused across executions, its per-statement state
keeps accumulating: bound tables (`tables`, `oneLevelTables`,
`mtmvRelatedTables`, `insertTargetTables`, `viewInfos`), CTE maps,
statistics (`relationIdToStatisticsMap`, `tableIdMapping`), MV/partition
rewrite state (`mvCanRewritePartitionsMap`, `tmpPlanForMvRewrite`,
`materializationRewrittenSuccessSet`), MVCC snapshots, connector write
schemas, placeholder bindings (`idToPlaceholderRealExpr`), etc. On
long-lived connections with a high number of `EXECUTE`s, these maps only
grow and are never released until the connection closes, which can OOM
the FE.
**Root cause:** the `StatementContext` stored in
`PreparedStatementContext` was treated as a permanent
per-prepared-statement object and reused, so state that should be
per-execution lived as long as the connection.
**Fix:** instead of reusing (and clearing in place) the same
`StatementContext`, allocate a brand-new context on every `EXECUTE` and
carry over only the state that must survive between executions:
- **ID generator positions** — so ids generated during this execution
never collide with ids already present in the cached analyzed plan from
`PREPARE`;
- **placeholder real expressions** bound by the protocol layer for this
`EXECUTE` (`idToPlaceholderRealExpr`) — this is the piece that prevents
the #63920 parameter-mismatch regression;
- the **placeholder → comparison-slot registry** (`idToComparisonSlot`)
used by the short-circuit fast path;
- the **placeholder list**;
- the **short-circuit / nondeterministic flags** that gate the
short-circuit fast path before any re-planning.
After the swap, the previous context becomes unreachable and is promptly
GC'd, so memory no longer grows with the number of executions. The
cached analyzed plan and the point-query (short-circuit) cache live on
`PrepareCommand` and `PreparedStatementContext` respectively, so they
keep being reused across executions.
**Changes:**
- `IdGenerator`: add `getCurrentId()` so a fresh context can continue
the id generators from the previous one.
- `StatementContext`: add `createNextExecuteContext()` which allocates
the fresh context and copies over the cross-execution state above.
- `PreparedStatementContext`: add `nextStatementContext()` which swaps
in the fresh context so the old one is released.
- `ExecuteCommand`: `run()` now uses the fresh per-execution context
(and the now-redundant in-place
`resetConnectorStatementScope()`/`resetMvccSnapshots()` calls are
removed since a fresh context starts empty by construction).
- Unit tests updated to assert the fresh-context behavior
(`ExecuteCommandTest`, `ConnectorStatementScopeTest`).
None
---
.../java/org/apache/doris/common/IdGenerator.java | 8 ++
.../org/apache/doris/nereids/StatementContext.java | 47 ++++++++++
.../trees/plans/commands/ExecuteCommand.java | 17 ++--
.../apache/doris/qe/PreparedStatementContext.java | 24 ++++++
.../connector/ConnectorStatementScopeTest.java | 51 +++++++----
.../trees/plans/commands/ExecuteCommandTest.java | 99 +++++++++++++++++++---
6 files changed, 213 insertions(+), 33 deletions(-)
diff --git
a/fe/fe-common/src/main/java/org/apache/doris/common/IdGenerator.java
b/fe/fe-common/src/main/java/org/apache/doris/common/IdGenerator.java
index 76f503bb5d7..aa769cf3415 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/IdGenerator.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/IdGenerator.java
@@ -33,6 +33,14 @@ public abstract class IdGenerator<IdType extends Id<IdType>>
{
return this;
}
+ /**
+ * The id value that {@link #getNextId()} would hand out next. Used to
seed a fresh
+ * per-execution id generator from an existing one so ids never collide.
+ */
+ public int getCurrentId() {
+ return nextId;
+ }
+
public abstract IdType getNextId();
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
index 0ea2e7cb025..64670a7398b 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
@@ -400,6 +400,53 @@ public class StatementContext implements Closeable {
}
}
+ /**
+ * Create a fresh StatementContext for the next EXECUTE of a prepared
statement.
+ *
+ * <p>A prepared statement keeps its StatementContext inside {@code
PreparedStatementContext}
+ * for the whole lifetime of the connection. Reusing the same object
across executions makes
+ * its per-statement state (bound tables, CTE maps, statistics, snapshots,
connector scope,
+ * ...) accumulate and it is only released when the connection closes,
which can OOM
+ * long-lived connections. Instead of clearing in place, allocate a
brand-new context per
+ * EXECUTE and copy over only the state that must survive between
executions, so the previous
+ * context becomes unreachable and is promptly GC'd.
+ *
+ * <p>Carried over:
+ * <ul>
+ * <li>id generator positions, so ids generated during this execution
never collide with
+ * ids already present in the cached analyzed plan from PREPARE;</li>
+ * <li>the placeholder real expressions bound by this EXECUTE (the
protocol layer fills
+ * them on the previous context before this method runs) and the
placeholder list;</li>
+ * <li>the placeholder to comparison-slot registry used by the
short-circuit fast path;</li>
+ * <li>the short-circuit and nondeterministic flags that gate the
short-circuit fast path
+ * before this execution re-plans.</li>
+ * </ul>
+ * Everything else (tables, CTEs, statistics, snapshots, planner
resources, connector
+ * scope, ...) starts empty/fresh on the new context.
+ */
+ public StatementContext createNextExecuteContext() {
+ // Continue the id generators from the previous context. The cached
analyzed plan from
+ // PREPARE (and every prior execution) already consumed ids from them,
so a fresh
+ // generator starting at 0 would collide with those ids during this
execution's planning.
+ StatementContext next = new StatementContext(connectContext,
originStatement,
+ exprIdGenerator.getCurrentId());
+ next.objectIdGenerator.resetId(objectIdGenerator.getCurrentId());
+ next.relationIdGenerator.resetId(relationIdGenerator.getCurrentId());
+ next.cteIdGenerator.resetId(cteIdGenerator.getCurrentId());
+ next.talbeIdGenerator.resetId(talbeIdGenerator.getCurrentId());
+
next.placeHolderIdGenerator.resetId(placeHolderIdGenerator.getCurrentId());
+ // Placeholder bindings of this EXECUTE, and the comparison-slot
registry used to replace
+ // conjuncts on the cached short-circuit plan without re-planning.
+ next.idToPlaceholderRealExpr.putAll(idToPlaceholderRealExpr);
+ next.idToComparisonSlot.putAll(idToComparisonSlot);
+ next.placeholders = new ArrayList<>(placeholders);
+ // Short-circuit gating flags are computed by the previous execution's
planning and gate
+ // the fast path of this execution before any re-planning happens.
+ next.isShortCircuitQuery = isShortCircuitQuery;
+ next.hasNondeterministic = hasNondeterministic;
+ return next;
+ }
+
public void setNeedLockTables(boolean needLockTables) {
this.needLockTables = needLockTables;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
index dd7b551585c..ee09d2a7d1a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
@@ -90,14 +90,14 @@ public class ExecuteCommand extends Command {
"prepare statement " + stmtName + " not found, maybe
expired");
}
PrepareCommand prepareCommand = preparedStmtCtx.command;
- StatementContext statementContext =
preparedStmtCtx.getStatementContext();
+ // Allocate a fresh StatementContext per EXECUTE so the per-statement
state accumulated by
+ // prior executions (bound tables, CTE maps, statistics, snapshots,
...) is released
+ // promptly instead of living as long as the connection, which can OOM
long-lived
+ // connections. The necessary cross-execution state (placeholder
bindings, comparison
+ // slots, id generator positions, short-circuit flags) is carried over
to the new context.
+ StatementContext statementContext =
preparedStmtCtx.nextStatementContext();
statementContext.setPrepareStage(false);
statementContext.setIsInsert(false);
- // A prepared EXECUTE reuses this one StatementContext across
executions; drop the connector
- // per-statement scope so a prior execution's cached tables/state
never leak into this one (the
- // scope key's queryId is a second line of defense). See
StatementContext#resetConnectorStatementScope.
- statementContext.resetConnectorStatementScope();
- statementContext.resetMvccSnapshots();
LogicalPlan logicalPlan = prepareCommand.getLogicalPlan();
List<LogicalPlan> relationRoots = new ArrayList<>();
if (logicalPlan instanceof InsertIntoTableCommand) {
@@ -157,6 +157,11 @@ public class ExecuteCommand extends Command {
&& hasShortCircuitContext
&& shortCircuitContextReusable
&& !statementContext.hasNondeterministic()) {
+ // The fresh per-execution context carries the short-circuit flag
but not the cached plan.
+ // Install the just-validated cache before the direct path: result
sending reads it via
+ // statementContext.getShortCircuitQueryContext(), and the
fallback (building one from a
+ // null planner, since this path skips planning) would NPE.
+
statementContext.setShortCircuitQueryContext(preparedStmtCtx.shortCircuitQueryContext.get());
PointQueryExecutor.directExecuteShortCircuitQuery(executor,
preparedStmtCtx, statementContext);
return;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/PreparedStatementContext.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/PreparedStatementContext.java
index fd1b093ee12..f28f6bd1c53 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/PreparedStatementContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/PreparedStatementContext.java
@@ -54,6 +54,30 @@ public class PreparedStatementContext {
this.statementContext = statementContext;
}
+ /**
+ * Allocate a fresh StatementContext for this EXECUTE and replace the
previous one, so the
+ * old context (with the per-statement state accumulated by prior
executions: bound tables,
+ * CTE maps, statistics, snapshots, ...) becomes unreachable and is
promptly GC'd.
+ *
+ * <p>A prepared statement lives as long as its connection. Reusing one
StatementContext
+ * across all executions would keep growing those maps and could OOM
long-lived connections,
+ * so we create a new object per execution and carry over only the state
that must survive
+ * (placeholder bindings, comparison slots, id generator positions,
short-circuit flags).
+ *
+ * @return the fresh StatementContext to use for the current execution
+ */
+ public StatementContext nextStatementContext() {
+ // Close the outgoing context's per-statement connector scope before
dropping it. The binary
+ // COM_STMT_EXECUTE path has no per-statement StatementContext.close()
finally (that only
+ // runs for COM_QUERY), and coordinated scans may not have registered
a query-finish
+ // callback yet (connector commands and failures before scan
registration have none).
+ // Without this, the outgoing scope's closeable connector metadata /
active connector
+ // transactions would be abandoned, and GC cannot finalize them.
+ statementContext.resetConnectorStatementScope();
+ statementContext = statementContext.createNextExecuteContext();
+ return statementContext;
+ }
+
public void setStartTime() {
startTime = System.currentTimeMillis();
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorStatementScopeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorStatementScopeTest.java
index 32a38415658..428ac2a2b80 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorStatementScopeTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorStatementScopeTest.java
@@ -45,9 +45,10 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests for the per-statement {@link ConnectorStatementScope}: the {@link
ConnectorStatementScope#NONE} no-op,
* the memoizing {@link ConnectorStatementScopeImpl}, the {@link
StatementContext} hosting + per-execution
- * reset a reused prepared statement relies on, and that {@link
ExecuteCommand} actually invokes that reset on
- * every execution (external/connector tables are planned through the reused
prepared context, so a missing
- * reset would leak one execution's loaded table into the next — see {@code
executeCommandResetsConnectorScope*}).
+ * reset primitive, and that {@link ExecuteCommand} allocates a fresh
StatementContext (and therefore a fresh
+ * connector scope) on every execution (external/connector tables are planned
through the context, so a missing
+ * fresh context would leak one execution's loaded table into the next — see
+ * {@code executeCommandAllocatesFreshConnectorScopePerExecution}).
*/
public class ConnectorStatementScopeTest {
@@ -184,21 +185,27 @@ public class ConnectorStatementScopeTest {
}
@Test
- public void executeCommandResetsConnectorScopePerExecution() throws
Exception {
- // WIRING test: the reset above is only load-bearing if
ExecuteCommand.run() actually calls it. A prepared
- // statement reuses ONE StatementContext across every EXECUTE, and an
EXTERNAL/connector table is planned
- // through that reused context each execution (external tables never
take the OLAP short-circuit fast path;
- // they always fall to the normal executor.execute() planner, which
re-resolves the table per execution).
- // So run() must drop the connector per-statement scope at the top of
every execution, or a prior execution's
- // memoized (loaded) table leaks into the next. The tests above cover
only the reset PRIMITIVE; this covers
- // that the command invokes it. MUTATION: delete
`statementContext.resetConnectorStatementScope()` from
- // ExecuteCommand.run() -> the seeded value survives -> the
assertNotSame below flips -> red.
+ public void executeCommandAllocatesFreshConnectorScopePerExecution()
throws Exception {
+ // WIRING test: a prepared statement lives as long as its connection,
so ExecuteCommand must not let one
+ // execution's connector state leak into the next. Previously run()
reused ONE StatementContext and reset
+ // its connector per-statement scope at the top of every execution;
now it allocates a brand-new
+ // StatementContext per EXECUTE (dropping the previous one, whose
per-statement maps were the OOM source
+ // on long-lived connections), so the fresh context starts with a
brand-new connector scope by
+ // construction. External/connector tables never take the OLAP
short-circuit fast path; they always fall
+ // to the normal executor.execute() planner, which re-resolves the
table per execution through that fresh
+ // context. MUTATION: reverting to reusing one StatementContext across
executions (without a reset) -> the
+ // seeded value survives in the context still used by the next
execution -> the assertNotSame below flips
+ // -> red.
StatementContext statementContext = new StatementContext();
- // Seed a value the way a first execution's connector planning would
(one loaded table the statement shares).
+ // Seed values the way a first execution's connector planning would
(one loaded table the statement
+ // shares, plus closeable metadata that must be finalized when the
context is dropped).
ConnectorStatementScope firstScope =
statementContext.getOrCreateConnectorStatementScope();
Object memoizedTable = firstScope.computeIfAbsent("table:1",
Object::new);
+ AtomicInteger closes = new AtomicInteger();
+ AutoCloseable closeable = () -> closes.incrementAndGet();
+ firstScope.computeIfAbsent("closeable:1", () -> closeable);
- // A prepared statement wrapping that reused StatementContext, with a
plain (non-cache, non-insert) plan.
+ // A prepared statement wrapping that StatementContext, with a plain
(non-cache, non-insert) plan.
LogicalPlan plan = Mockito.mock(LogicalPlan.class);
PrepareCommand prepareCommand = Mockito.mock(PrepareCommand.class);
Mockito.when(prepareCommand.getLogicalPlan()).thenReturn(plan);
@@ -213,15 +220,25 @@ public class ConnectorStatementScopeTest {
Mockito.when(ctx.getSessionVariable()).thenReturn(sessionVariable);
Mockito.when(ctx.getStatementContext()).thenReturn(statementContext);
- // A no-op executor: we pin the reset wiring, not the planner.
execute() is a mock no-op.
+ // A no-op executor: we pin the fresh-context wiring, not the planner.
execute() is a mock no-op.
StmtExecutor executor = Mockito.mock(StmtExecutor.class);
Mockito.when(executor.getContext()).thenReturn(ctx);
new ExecuteCommand("s", prepareCommand, statementContext).run(ctx,
executor);
- ConnectorStatementScope secondScope =
statementContext.getOrCreateConnectorStatementScope();
+ // run() must swap in a fresh StatementContext for this execution,
releasing the previous one...
+ StatementContext nextContext = preparedStmtCtx.getStatementContext();
+ Assertions.assertNotSame(statementContext, nextContext,
+ "ExecuteCommand allocates a fresh StatementContext per EXECUTE
so the old one is released");
+ // ...finalizing the outgoing scope's closeable connector values
(COM_STMT_EXECUTE has no per-statement
+ // StatementContext.close() finally, so dropping the context must
close the scope, or closeable metadata
+ // and active connector transactions would be abandoned and never
finalized by GC).
+ Assertions.assertEquals(1, closes.get(),
+ "dropping the outgoing context finalizes its closeable
connector values");
+ // ...and whose connector scope is brand-new, so a prior execution's
memoized connector table cannot leak.
+ ConnectorStatementScope secondScope =
nextContext.getOrCreateConnectorStatementScope();
Assertions.assertNotSame(firstScope, secondScope,
- "ExecuteCommand drops the reused context's connector scope so
the next execution starts fresh");
+ "the fresh context starts with a fresh connector scope");
Assertions.assertNotSame(memoizedTable,
secondScope.computeIfAbsent("table:1", Object::new),
"a prior execution's memoized connector table must not leak
into the next EXECUTE");
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
index 8872c6e7b65..7905b31b5ef 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
@@ -17,9 +17,12 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.analysis.DescriptorTable;
+import org.apache.doris.analysis.Queriable;
import org.apache.doris.analysis.TableScanParams;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.TableIf;
@@ -32,11 +35,15 @@ import org.apache.doris.nereids.parser.NereidsParser;
import org.apache.doris.nereids.trees.expressions.SubqueryExpr;
import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.planner.OlapScanNode;
+import org.apache.doris.planner.Planner;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.OriginStatement;
import org.apache.doris.qe.PreparedStatementContext;
import org.apache.doris.qe.SessionVariable;
+import org.apache.doris.qe.ShortCircuitQueryContext;
import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.thrift.TQueryOptions;
import com.google.common.collect.ImmutableMap;
import org.junit.jupiter.api.Assertions;
@@ -141,11 +148,11 @@ public class ExecuteCommandTest {
@Test
public void
testPreparedConnectorUpdateRefreshesWriteDefaultEveryExecution() throws
Exception {
- // Prepared UPDATE reuses one StatementContext. Model connector
metadata changing from default 1 to 2
- // between executions: each planner callback pins the current schema
only when no schema is already pinned,
- // then expands DEFAULT(v) and writes the resulting value.
- // MUTATION: resetConnectorStatementScope() not clearing
connectorWriteSchemas makes execution two reuse
- // default 1, so the written values become [1, 1] instead of [1, 2].
+ // ExecuteCommand allocates a fresh StatementContext per EXECUTE.
Model connector metadata changing from
+ // default 1 to 2 between executions: each execution's planner
callback pins the current schema only when
+ // the fresh context has no schema pinned, then expands DEFAULT(v) and
writes the resulting value.
+ // MUTATION: reusing one StatementContext across executions without
dropping connectorWriteSchemas makes
+ // execution two reuse default 1, so the written values become [1, 1]
instead of [1, 2].
String sql = "update ext_catalog.db.t set v = default(v) where id = 1";
LogicalPlan logicalPlan = new NereidsParser().parseSingle(sql);
Assertions.assertInstanceOf(UpdateCommand.class, logicalPlan);
@@ -166,12 +173,16 @@ public class ExecuteCommandTest {
AtomicInteger metadataDefault = new AtomicInteger(1);
List<String> writtenValues = new ArrayList<>();
Mockito.doAnswer(invocation -> {
- if
(!statementContext.getConnectorWriteSchema(tableId).isPresent()) {
+ // Each execution plans through the fresh StatementContext
allocated by ExecuteCommand, so resolve/pin
+ // the connector writer schema on THAT context (a stale pin would
make the second execution reuse
+ // default 1).
+ StatementContext currentContext =
preparedStatement.getStatementContext();
+ if (!currentContext.getConnectorWriteSchema(tableId).isPresent()) {
Column column = new Column("v",
ScalarType.createType(PrimitiveType.INT),
false, null, String.valueOf(metadataDefault.get()),
"");
- statementContext.setConnectorWriteSchema(tableId,
Collections.singletonList(column));
+ currentContext.setConnectorWriteSchema(tableId,
Collections.singletonList(column));
}
-
writtenValues.add(statementContext.getConnectorWriteSchema(tableId).get()
+
writtenValues.add(currentContext.getConnectorWriteSchema(tableId).get()
.get(0).getDefaultValueSql());
return null;
}).when(executor).execute();
@@ -219,13 +230,81 @@ public class ExecuteCommandTest {
statementContext.getSnapshot(table, Optional.empty(),
Optional.empty()).orElse(null));
new ExecuteCommand("stmt", prepareCommand,
statementContext).run(connectContext, executor);
- statementContext.loadSnapshots(table, Optional.empty(),
Optional.empty());
+
+ // ExecuteCommand allocates a fresh StatementContext per EXECUTE, so
the next execution must not reuse the
+ // snapshot pinned on the previous context (a stale snapshot would
make a later commit permanently
+ // invisible).
+ StatementContext nextContext = preparedStatement.getStatementContext();
+ Assertions.assertNotSame(statementContext, nextContext,
+ "ExecuteCommand allocates a fresh StatementContext per
EXECUTE");
+ nextContext.loadSnapshots(table, Optional.empty(), Optional.empty());
Assertions.assertSame(second,
- statementContext.getSnapshot(table, Optional.empty(),
Optional.empty()).orElse(null));
+ nextContext.getSnapshot(table, Optional.empty(),
Optional.empty()).orElse(null));
Mockito.verify(table, Mockito.times(2)).loadSnapshot(Optional.empty(),
Optional.empty());
}
+ @Test
+ public void
testFastPathInstallsCachedShortCircuitContextAcrossExecutions() throws
Exception {
+ // ExecuteCommand allocates a fresh StatementContext per EXECUTE. The
fresh context carries the
+ // short-circuit flag but not the cached plan, so the fast path must
install the just-validated
+ // ShortCircuitQueryContext before direct execution -- otherwise
result sending falls back to
+ // `new ShortCircuitQueryContext(planner, ...)` with a null planner
(this path never plans) and
+ // NPEs on planner.getDescTable(). Two executions exercise the second
(reusable) EXECUTE that
+ // hits the regression.
+ // MUTATION: removing the install in ExecuteCommand.run() -> the fresh
context has no
+ // statement-level cache -> the assertSame below flips -> red.
+ String sql = "select * from tbl";
+ LogicalPlan logicalPlan = new NereidsParser().parseSingle(sql);
+
+ ConnectContext connectContext = Mockito.mock(ConnectContext.class);
+ StatementContext statementContext = new StatementContext();
+ statementContext.setShortCircuitQuery(true);
+ PrepareCommand prepareCommand = new PrepareCommand(
+ "stmt", logicalPlan, Collections.emptyList(), new
OriginStatement(sql, 0));
+ PreparedStatementContext preparedStatement = new
PreparedStatementContext(
+ prepareCommand, connectContext, statementContext, "stmt");
+
+ // A real ShortCircuitQueryContext (built from a mocked planner) that
passes isReusable().
+ Planner planner = Mockito.mock(Planner.class);
+ Mockito.when(planner.getQueryOptions()).thenReturn(new
TQueryOptions());
+ DescriptorTable descriptorTable = new DescriptorTable();
+ descriptorTable.createTupleDescriptor();
+ Mockito.when(planner.getDescTable()).thenReturn(descriptorTable);
+ OlapScanNode scanNode = Mockito.mock(OlapScanNode.class);
+ OlapTable table = Mockito.spy(new OlapTable());
+ Mockito.doReturn("tbl").when(table).getName();
+ Mockito.doReturn(10).when(table).getBaseSchemaVersion();
+
Mockito.when(scanNode.getPointQueryProjectList()).thenReturn(Collections.emptyList());
+ Mockito.when(scanNode.getOlapTable()).thenReturn(table);
+ Mockito.when(scanNode.getTableNameInPlan()).thenReturn("tbl");
+
Mockito.when(scanNode.getConjuncts()).thenReturn(Collections.emptyList());
+
Mockito.when(planner.getScanNodes()).thenReturn(Collections.singletonList(scanNode));
+ ShortCircuitQueryContext cachedPlan = new
ShortCircuitQueryContext(planner, Mockito.mock(Queriable.class));
+ preparedStatement.shortCircuitQueryContext = Optional.of(cachedPlan);
+
+ StmtExecutor executor = Mockito.mock(StmtExecutor.class);
+
Mockito.when(connectContext.getPreparedStementContext("stmt")).thenReturn(preparedStatement);
+ SessionVariable sessionVariable = new SessionVariable();
+ sessionVariable.enableGroupCommitFullPrepare = false;
+
Mockito.when(connectContext.getSessionVariable()).thenReturn(sessionVariable);
+
Mockito.when(connectContext.getStatementContext()).thenReturn(statementContext);
+ Mockito.when(executor.getContext()).thenReturn(connectContext);
+
+ ExecuteCommand execute = new ExecuteCommand("stmt", prepareCommand,
statementContext);
+ execute.run(connectContext, executor);
+ Assertions.assertSame(cachedPlan,
preparedStatement.getStatementContext().getShortCircuitQueryContext(),
+ "the fast path installs the validated cache on the fresh
context (first EXECUTE)");
+ Mockito.verify(executor,
Mockito.times(1)).executeAndSendResult(Mockito.anyBoolean(),
Mockito.anyBoolean(),
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+
+ execute.run(connectContext, executor);
+ Assertions.assertSame(cachedPlan,
preparedStatement.getStatementContext().getShortCircuitQueryContext(),
+ "the fast path installs the validated cache on the fresh
context (second, reusable EXECUTE)");
+ Mockito.verify(executor,
Mockito.times(2)).executeAndSendResult(Mockito.anyBoolean(),
Mockito.anyBoolean(),
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+ }
+
private String resolveNextSnapshot(TableScanParams scanParams,
AtomicInteger snapshotId) {
return scanParams.getOrResolveMapParams(ignored -> ImmutableMap.of(
"scan.snapshot-id",
String.valueOf(snapshotId.incrementAndGet())))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]