diff --git a/iotdb-core/datanode/pom.xml b/iotdb-core/datanode/pom.xml
index f0c2d609fc3fc..ef9e3a037271f 100644
--- a/iotdb-core/datanode/pom.xml
+++ b/iotdb-core/datanode/pom.xml
@@ -29,6 +29,7 @@
iotdb-serverIoTDB: Core: Data-Node (Server)
+ en${iotdb.test.skip}false${iotdb.test.skip}
@@ -372,6 +373,19 @@
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+
+
+
+ src/main/java/**/*.java
+ src/test/java/**/*.java
+ src/main/i18n/**/*.java
+
+
+
+
@@ -499,6 +513,7 @@
${project.build.directory}/generated-sources/freemarker
+ ${project.basedir}/src/main/i18n/${i18n.locale}
@@ -536,6 +551,12 @@
+
+ with-zh-locale
+
+ zh
+
+ skipIoTDBTests
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 0000000000000..1c4433d3b552e
--- /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 0000000000000..5b151ce3e351b
--- /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 e2103817b24b1..73d4093e6c7d6 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 @@
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.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 @@ private void resetResultSetColumnMemoryTracking() {
schemaFetchDeserializedColumnCount = 0;
}
- private MemoryNotEnoughException enrichResultSetColumnMemoryNotEnoughException(
+ MemoryNotEnoughException enrichResultSetColumnMemoryNotEnoughException(
MemoryNotEnoughException e, long requestedBytes) {
if (!resultSetColumnMemoryTrackingEnabled
|| (matchedSourceColumnsForResultSet == 0
@@ -527,10 +529,23 @@ private MemoryNotEnoughException enrichResultSetColumnMemoryNotEnoughException(
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 @@ private MemoryNotEnoughException enrichResultSetColumnMemoryNotEnoughException(
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 @@ private MemoryNotEnoughException enrichResultSetColumnMemoryNotEnoughException(
: FOR_QUERY_ENGINE_OPERATOR_MEMORY_POOL,
formatBytes(sourceColumnMemoryCostForResultSet),
formatBytes(generatedResultSetColumnMemoryCost),
- formatBytes(requestedBytes),
+ formatBytes(failedReservationBytes),
formatBytes(freeBytes),
e.getMessage()));
}
@@ -608,6 +626,26 @@ private long estimateExceededColumns(long freeBytes, long requestedBytes) {
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 b8aef336e1010..04dc7099539da 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.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.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 long reserveFromFreeMemoryForOperators(
}
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 0000000000000..8e8c6749559d7
--- /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 2d717f0fc65e1..6d5cd68e04989 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 @@
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 void resultSetColumnMemoryNotEnoughExceptionContainsColumnDiagnostics() {
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"));