This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/dev/1.3 by this push:
new 414aa9eff66 [Query] Fix result-column OOM diagnostics (#18661) (#18667)
414aa9eff66 is described below
commit 414aa9eff66310c389764ec1640ee4aac8512155
Author: Caideyipi <[email protected]>
AuthorDate: Mon Sep 21 11:48:13 2026 +0800
[Query] Fix result-column OOM diagnostics (#18661) (#18667)
(cherry picked from commit 25bf38eba0cc9d3d213b79068ea002e62c5ced96)
---
iotdb-core/datanode/pom.xml | 21 ++++++++
.../iotdb/db/i18n/DataNodeQueryMessages.java | 35 +++++++++++++
.../iotdb/db/i18n/DataNodeQueryMessages.java | 32 ++++++++++++
.../db/queryengine/common/MPPQueryContext.java | 48 +++++++++++++++--
.../plan/planner/LocalExecutionPlanner.java | 19 ++++---
.../memory/OperatorMemoryNotEnoughException.java | 43 ++++++++++++++++
.../db/queryengine/common/MPPQueryContextTest.java | 60 ++++++++++++++++++++++
7 files changed, 247 insertions(+), 11 deletions(-)
diff --git a/iotdb-core/datanode/pom.xml b/iotdb-core/datanode/pom.xml
index f0c2d609fc3..ef9e3a03727 100644
--- a/iotdb-core/datanode/pom.xml
+++ b/iotdb-core/datanode/pom.xml
@@ -29,6 +29,7 @@
<artifactId>iotdb-server</artifactId>
<name>IoTDB: Core: Data-Node (Server)</name>
<properties>
+ <i18n.locale>en</i18n.locale>
<iotdb.it.skip>${iotdb.test.skip}</iotdb.it.skip>
<iotdb.test.skip>false</iotdb.test.skip>
<iotdb.ut.skip>${iotdb.test.skip}</iotdb.ut.skip>
@@ -372,6 +373,19 @@
</dependencies>
<build>
<plugins>
+ <plugin>
+ <groupId>com.diffplug.spotless</groupId>
+ <artifactId>spotless-maven-plugin</artifactId>
+ <configuration>
+ <java>
+ <includes>
+ <include>src/main/java/**/*.java</include>
+ <include>src/test/java/**/*.java</include>
+ <include>src/main/i18n/**/*.java</include>
+ </includes>
+ </java>
+ </configuration>
+ </plugin>
<!--using `mvn test` to run UT, `mvn verify` to run ITs
Reference:
https://antoniogoncalves.org/2012/12/13/lets-turn-integration-tests-with-maven-to-a-first-class-citizen/-->
<plugin>
@@ -499,6 +513,7 @@
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/freemarker</source>
+
<source>${project.basedir}/src/main/i18n/${i18n.locale}</source>
</sources>
</configuration>
</execution>
@@ -536,6 +551,12 @@
</plugins>
</build>
<profiles>
+ <profile>
+ <id>with-zh-locale</id>
+ <properties>
+ <i18n.locale>zh</i18n.locale>
+ </properties>
+ </profile>
<profile>
<id>skipIoTDBTests</id>
<activation>
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
new file mode 100644
index 00000000000..1c4433d3b55
--- /dev/null
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.i18n;
+
+public final class DataNodeQueryMessages {
+
+ private DataNodeQueryMessages() {}
+
+ public static final String RESULT_SET_COLUMN_MEMORY_SHORTAGE_EQUIVALENT =
+ "The failed memory reservation exceeds available memory by the
equivalent of at least "
+ + "%,d columns, estimated from the observed average column size. ";
+
+ public static final String
+
QUERY_EXCEPTION_THERE_IS_NOT_ENOUGH_MEMORY_FOR_QUERY_S_THE_CONTEXTHOLDER_546CDD02
=
+ "There is not enough memory for Query %s, the contextHolder is
%s,current remaining free "
+ + "memory is %dB, already reserved memory for this context in
total is %dB, the memory "
+ + "requested this time is %dB";
+}
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
new file mode 100644
index 00000000000..5b151ce3e35
--- /dev/null
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.i18n;
+
+public final class DataNodeQueryMessages {
+
+ private DataNodeQueryMessages() {}
+
+ public static final String RESULT_SET_COLUMN_MEMORY_SHORTAGE_EQUIVALENT =
+ "本次失败的内存申请超出可用内存,按已记录列的平均内存估算,至少超出相当于 %,d 列的容量。";
+
+ public static final String
+
QUERY_EXCEPTION_THERE_IS_NOT_ENOUGH_MEMORY_FOR_QUERY_S_THE_CONTEXTHOLDER_546CDD02
=
+ "Query %s 内存不足,contextHolder 为 %s,当前剩余空闲内存为 %dB,该 context 已预留总内存为
%dB,本次请求内存为 %dB。";
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
index e2103817b24..73d4093e6c7 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.db.queryengine.common;
import org.apache.iotdb.common.rpc.thrift.TEndPoint;
import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
import org.apache.iotdb.commons.utils.TestOnly;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
import org.apache.iotdb.db.queryengine.exception.MemoryNotEnoughException;
import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
import org.apache.iotdb.db.queryengine.plan.analyze.PredicateUtils;
@@ -31,6 +32,7 @@ import
org.apache.iotdb.db.queryengine.plan.analyze.lock.SchemaLockType;
import org.apache.iotdb.db.queryengine.plan.planner.LocalExecutionPlanner;
import
org.apache.iotdb.db.queryengine.plan.planner.memory.MemoryReservationManager;
import
org.apache.iotdb.db.queryengine.plan.planner.memory.NotThreadSafeMemoryReservationManager;
+import
org.apache.iotdb.db.queryengine.plan.planner.memory.OperatorMemoryNotEnoughException;
import org.apache.iotdb.db.queryengine.statistics.QueryPlanStatistics;
import org.apache.tsfile.read.filter.basic.Filter;
@@ -518,7 +520,7 @@ public class MPPQueryContext {
schemaFetchDeserializedColumnCount = 0;
}
- private MemoryNotEnoughException
enrichResultSetColumnMemoryNotEnoughException(
+ MemoryNotEnoughException enrichResultSetColumnMemoryNotEnoughException(
MemoryNotEnoughException e, long requestedBytes) {
if (!resultSetColumnMemoryTrackingEnabled
|| (matchedSourceColumnsForResultSet == 0
@@ -527,10 +529,23 @@ public class MPPQueryContext {
return e;
}
- long freeBytes =
LocalExecutionPlanner.getInstance().getFreeMemoryForOperators();
+ long freeBytes =
+ e instanceof OperatorMemoryNotEnoughException
+ ? ((OperatorMemoryNotEnoughException) e).getFreeBytes()
+ : LocalExecutionPlanner.getInstance().getFreeMemoryForOperators();
+ long failedReservationBytes =
+ e instanceof OperatorMemoryNotEnoughException
+ ? ((OperatorMemoryNotEnoughException) e).getRequestedBytes()
+ : requestedBytes;
long shortageBytes =
- requestedBytes > 0 && requestedBytes > freeBytes ? requestedBytes -
freeBytes : -1;
+ failedReservationBytes > 0 && failedReservationBytes > freeBytes
+ ? failedReservationBytes - freeBytes
+ : -1;
long exceededColumns = estimateExceededColumns(freeBytes, requestedBytes);
+ long columnEquivalentShortage =
+ exceededColumns > 0
+ ? 0
+ : estimateColumnEquivalentShortage(freeBytes,
failedReservationBytes, requestedBytes);
return new MemoryNotEnoughException(
String.format(
@@ -542,7 +557,10 @@ public class MPPQueryContext {
exceededColumns > 0
? String.format(
Locale.ROOT, RESULT_SET_COLUMNS_EXCEED_MEMORY_CAPACITY,
exceededColumns)
- : "",
+ : String.format(
+ Locale.ROOT,
+
DataNodeQueryMessages.RESULT_SET_COLUMN_MEMORY_SHORTAGE_EQUIVALENT,
+ columnEquivalentShortage),
formatSeriesPaginationForDiagnostics(),
alignByDeviceForResultSetColumnTracking
? ""
@@ -552,7 +570,7 @@ public class MPPQueryContext {
: FOR_QUERY_ENGINE_OPERATOR_MEMORY_POOL,
formatBytes(sourceColumnMemoryCostForResultSet),
formatBytes(generatedResultSetColumnMemoryCost),
- formatBytes(requestedBytes),
+ formatBytes(failedReservationBytes),
formatBytes(freeBytes),
e.getMessage()));
}
@@ -608,6 +626,26 @@ public class MPPQueryContext {
return Math.max(0, columnsToCompare - estimatedCapacity);
}
+ /** Converts a failed batch's memory deficit into an observed-column-size
equivalent. */
+ private long estimateColumnEquivalentShortage(
+ long freeBytes, long failedReservationBytes, long requestedBytes) {
+ long avgColumnMemory;
+ if (generatedResultSetColumns > 0 && generatedResultSetColumnMemoryCost >
0) {
+ avgColumnMemory =
+ Math.max(1, divideCeil(generatedResultSetColumnMemoryCost,
generatedResultSetColumns));
+ } else if (expandedSourceColumnsForResultSet > 0 &&
sourceColumnMemoryCostForResultSet > 0) {
+ avgColumnMemory =
+ Math.max(
+ 1, divideCeil(sourceColumnMemoryCostForResultSet,
expandedSourceColumnsForResultSet));
+ } else {
+ avgColumnMemory = Math.max(1, requestedBytes > 0 ? requestedBytes :
failedReservationBytes);
+ }
+
+ // The failed allocation proves a shortage, even if memory was released
before it was read.
+ long shortageBytes = Math.max(1, failedReservationBytes - freeBytes);
+ return divideCeil(shortageBytes, avgColumnMemory);
+ }
+
private long estimateExceededSchemaFetchColumns(long freeBytes, long
requestedBytes) {
if (schemaFetchDeserializedColumnCount <= 0) {
return -1;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java
index b8aef336e10..04dc7099539 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlanner.java
@@ -22,6 +22,7 @@ import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.utils.TestOnly;
import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
import org.apache.iotdb.db.queryengine.common.DeviceContext;
import org.apache.iotdb.db.queryengine.exception.MemoryNotEnoughException;
import org.apache.iotdb.db.queryengine.execution.driver.DataDriverContext;
@@ -31,6 +32,7 @@ import
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceStateM
import org.apache.iotdb.db.queryengine.execution.operator.Operator;
import org.apache.iotdb.db.queryengine.metric.QueryRelatedResourceMetricSet;
import org.apache.iotdb.db.queryengine.plan.analyze.TypeProvider;
+import
org.apache.iotdb.db.queryengine.plan.planner.memory.OperatorMemoryNotEnoughException;
import
org.apache.iotdb.db.queryengine.plan.planner.memory.PipelineMemoryEstimator;
import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
import org.apache.iotdb.db.schemaengine.schemaregion.ISchemaRegion;
@@ -280,13 +282,18 @@ public class LocalExecutionPlanner {
}
long allocated = allocateOperatorsMemory(memoryInBytes, isHighestPriority);
if (allocated < 0) {
- throw new MemoryNotEnoughException(
+ long freeBytes = freeMemoryForOperators;
+ throw new OperatorMemoryNotEnoughException(
String.format(
- "There is not enough memory for Query %s, the contextHolder is
%s,"
- + "current remaining free memory is %dB, "
- + "already reserved memory for this context in total is %dB,
"
- + "the memory requested this time is %dB",
- queryId, contextHolder, freeMemoryForOperators, reservedBytes,
memoryInBytes));
+ DataNodeQueryMessages
+
.QUERY_EXCEPTION_THERE_IS_NOT_ENOUGH_MEMORY_FOR_QUERY_S_THE_CONTEXTHOLDER_546CDD02,
+ queryId,
+ contextHolder,
+ freeBytes,
+ reservedBytes,
+ memoryInBytes),
+ memoryInBytes,
+ freeBytes);
}
return allocated;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/OperatorMemoryNotEnoughException.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/OperatorMemoryNotEnoughException.java
new file mode 100644
index 00000000000..8e8c6749559
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/OperatorMemoryNotEnoughException.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.queryengine.plan.planner.memory;
+
+import org.apache.iotdb.db.queryengine.exception.MemoryNotEnoughException;
+
+/** Preserves the failed batch size and available memory for query-analysis
diagnostics. */
+public class OperatorMemoryNotEnoughException extends MemoryNotEnoughException
{
+
+ private final long requestedBytes;
+ private final long freeBytes;
+
+ public OperatorMemoryNotEnoughException(String message, long requestedBytes,
long freeBytes) {
+ super(message);
+ this.requestedBytes = requestedBytes;
+ this.freeBytes = freeBytes;
+ }
+
+ public long getRequestedBytes() {
+ return requestedBytes;
+ }
+
+ public long getFreeBytes() {
+ return freeBytes;
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/common/MPPQueryContextTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/common/MPPQueryContextTest.java
index 2d717f0fc65..6d5cd68e049 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/common/MPPQueryContextTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/common/MPPQueryContextTest.java
@@ -21,8 +21,12 @@ package org.apache.iotdb.db.queryengine.common;
import org.apache.iotdb.db.queryengine.exception.MemoryNotEnoughException;
import org.apache.iotdb.db.queryengine.plan.planner.LocalExecutionPlanner;
+import
org.apache.iotdb.db.queryengine.plan.planner.memory.OperatorMemoryNotEnoughException;
+import org.apache.iotdb.db.utils.ErrorHandlingUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Test;
public class MPPQueryContextTest {
@@ -60,6 +64,62 @@ public class MPPQueryContextTest {
assertContains(message, "Original error:");
}
+ @Test
+ public void
resultSetColumnMemoryNotEnoughExceptionReportsOverageAfterExpansion() {
+ // The old estimate returned zero when all (or all but one) source columns
were expanded.
+
Assume.assumeTrue(LocalExecutionPlanner.getInstance().getFreeMemoryForOperators()
> 0);
+ for (int unmatchedColumns = 0; unmatchedColumns <= 1; unmatchedColumns++) {
+ MPPQueryContext context =
+ new MPPQueryContext(new QueryId("result_column_overage_" +
unmatchedColumns));
+ context.initResultSetColumnMemoryTracking(0, 0, false);
+ context.recordMatchedSourceColumnsForResultSet(2 + unmatchedColumns);
+ context.recordExpandedSourceColumnForResultSet(1);
+ context.recordExpandedSourceColumnForResultSet(1);
+ context.recordGeneratedResultSetColumn(2);
+
+ MemoryNotEnoughException exception =
+ Assert.assertThrows(
+ MemoryNotEnoughException.class,
+ () ->
context.reserveMemoryForFrontEnd(requestLargerThanFreeOperatorMemory()));
+
+ String message = exception.getMessage();
+ assertContains(message, "expanded 2 source columns, and generated 1
result-set columns");
+ assertContains(message, "equivalent of at least 1 columns");
+ Assert.assertFalse(message, message.contains("The matched source columns
exceed"));
+ assertContains(message, "increase query memory by at least");
+ }
+ }
+
+ @Test
+ public void resultSetColumnMemoryNotEnoughExceptionUsesFailedBatchSize() {
+ MPPQueryContext context = new MPPQueryContext(new
QueryId("result_column_batch_oom_test"));
+ context.initResultSetColumnMemoryTracking(0, 0, false);
+ context.recordMatchedSourceColumnsForResultSet(2);
+ context.recordExpandedSourceColumnForResultSet(1);
+ context.recordExpandedSourceColumnForResultSet(1);
+ context.recordGeneratedResultSetColumn(2);
+
+ long failedBatchBytes = 1_048_840;
+ long freeBytesAtFailure = 702_452;
+ long lastExpressionBytes = 760;
+ MemoryNotEnoughException original =
+ new OperatorMemoryNotEnoughException(
+ "the memory requested this time is 1048840B", failedBatchBytes,
freeBytesAtFailure);
+
+ MemoryNotEnoughException exception =
+ context.enrichResultSetColumnMemoryNotEnoughException(original,
lastExpressionBytes);
+ String message = exception.getMessage();
+
+ Assert.assertEquals(
+ TSStatusCode.QUOTA_MEM_QUERY_NOT_ENOUGH.getStatusCode(),
+ ErrorHandlingUtils.onQueryException(exception, "query").getCode());
+ assertContains(message, "requested this time 1.00 MB (1048840 B)");
+ assertContains(message, "increase query memory by at least 338.27 KB
(346388 B)");
+ assertContains(message, "at least 173,194 columns");
+ assertContains(message, "the memory requested this time is 1048840B");
+ Assert.assertFalse(message, message.contains("requested this time 760 B"));
+ }
+
@Test
public void
schemaFetchMemoryNotEnoughExceptionContainsFetchedColumnDiagnostics() {
MPPQueryContext context = new MPPQueryContext(new
QueryId("schema_fetch_oom_test"));