This is an automated email from the ASF dual-hosted git repository.
hello-stephen 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 3a51246e419 [fix](fe test) Fix heap exhaustion in the table stats
snapshot test (#70525)
3a51246e419 is described below
commit 3a51246e419f195718b9bbbcb5959c9efa46141b
Author: shuke <[email protected]>
AuthorDate: Fri Nov 13 16:06:06 2026 +0800
[fix](fe test) Fix heap exhaustion in the table stats snapshot test (#70525)
### Background
The FE estimates a table's row count from the last analysis and the rows
loaded since then. A reader needs the collected count and its baseline
from the same snapshot.
`TableStatsMetaTest.testRowCountSnapshotIsCoherent` checks this while
another thread publishes analyses.
### What problem does this PR solve?
Issue Number: None
Related PR: #69028 (introduced the snapshot test)
PR Type: Test
Problem Summary:
The snapshot test intermittently exhausts the 1 GiB Surefire heap. Use a
stub-only table mock and run one read and one update per round for 5,000
rounds. The writer alternates two snapshots that both yield 150 rows, so
a reader that mixes their fields can now observe 130 or 170. Reader
exceptions propagate to JUnit.
### Details
- **Failure:** In a failing community FEUT run, both the reader and
writer threw `OutOfMemoryError: Java heap space` while calling the
mocked `OlapTable.getBaseIndexId()`. Surefire produced no XML for the
class.
- **Cause:** The original reader loop had no iteration limit. It
repeatedly called a getter on an ordinary Mockito mock until the writer
finished 5,000 updates. Mockito retained each call, even though the
getter always returned the same value. In a local 1 GiB reproduction,
the heap dump showed 2,136,117 entries in one mock invocation list.
- **Stability evidence:** A local pressure test issued 1,000 reads per
update using the FEUT classpath. The original fixture retained 502,015
calls after 500,000 reads and later ran out of heap. The fixed fixture
completed all 5,000,000 reads with zero recorded calls.
### Testing
- Ran `bash run-fe-ut.sh --coverage --run
'org.apache.doris.statistics.analysis.TableStatsMetaTest#testRowCountSnapshotIsCoherent'`
locally on JDK 17 with one fork and `-Xmx1024m`. The Maven build passed.
All 15 tests in the class also passed using the Surefire classpath, and
the target method passed in five fresh JVMs.
- Reverted only `stubOnly()` locally. The new history assertion failed
with 25,014 recorded calls. In a separate run, removing synchronization
from `getRowCountWithDeltaRows()` made JUnit report 130 rows where it
expected 150.
---
.../statistics/analysis/TableStatsMetaTest.java | 63 +++++++++++++---------
1 file changed, 38 insertions(+), 25 deletions(-)
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/statistics/analysis/TableStatsMetaTest.java
b/fe/fe-core/src/test/java/org/apache/doris/statistics/analysis/TableStatsMetaTest.java
index a3ae026a679..6f57c50a75e 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/statistics/analysis/TableStatsMetaTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/statistics/analysis/TableStatsMetaTest.java
@@ -35,7 +35,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.HashSet;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.Phaser;
class TableStatsMetaTest {
@@ -44,7 +45,8 @@ class TableStatsMetaTest {
private static final String BASE_INDEX_NAME = "baseIndex";
private OlapTable mockOlapTable() {
- OlapTable table = Mockito.mock(OlapTable.class);
+ // The snapshot test repeatedly calls these getters from both threads.
+ OlapTable table = Mockito.mock(OlapTable.class,
Mockito.withSettings().stubOnly());
Mockito.doReturn(Lists.newArrayList(BASE_INDEX_ID,
ROLLUP_INDEX_ID)).when(table).getIndexIdList();
Mockito.doReturn(BASE_INDEX_ID).when(table).getBaseIndexId();
Mockito.doReturn(BASE_INDEX_NAME).when(table).getIndexNameById(BASE_INDEX_ID);
@@ -55,15 +57,18 @@ class TableStatsMetaTest {
return table;
}
- // 100 rows of the base index had been loaded when the job was built.
private AnalysisInfo analyzeJob() {
- AnalysisInfo job = new AnalysisInfoBuilder().setRowCount(100)
+ return analyzeJob(100);
+ }
+
+ private AnalysisInfo analyzeJob(long collectedRows) {
+ AnalysisInfo job = new AnalysisInfoBuilder().setRowCount(collectedRows)
.setJobColumns(Sets.newHashSet(Pair.of(BASE_INDEX_NAME,
"col1")))
- .setUpdateRows(100)
+ .setUpdateRows(collectedRows)
.setTblUpdateTime(1000L)
.setJobType(AnalysisInfo.JobType.MANUAL)
.build();
- job.addIndexRowCount(BASE_INDEX_ID, 100);
+ job.addIndexRowCount(BASE_INDEX_ID, collectedRows);
return job;
}
@@ -233,31 +238,39 @@ class TableStatsMetaTest {
void testRowCountSnapshotIsCoherent() throws Exception {
OlapTable table = mockOlapTable();
TableStatsMeta meta = new TableStatsMeta(table);
- // An analysis collected the 100 rows the table had, then 50 more rows
were loaded.
- meta.update(analyzeJob(), table);
+ AnalysisInfo first = analyzeJob(100);
+ AnalysisInfo second = analyzeJob(120);
+ meta.update(first, table);
meta.updatedRows.set(150);
- AtomicBoolean incoherent = new AtomicBoolean(false);
- AtomicBoolean stop = new AtomicBoolean(false);
- Thread reader = new Thread(() -> {
- while (!stop.get()) {
- // Every state the writer goes through reports the 150 rows
the table holds.
- if (meta.getRowCountWithDeltaRows(table, BASE_INDEX_ID) !=
150) {
- incoherent.set(true);
+ // Both snapshots give 150 rows. Mixing the collected count from one
with the baseline of
+ // the other gives 130 or 170. Start each read and update in the same
round.
+ int rounds = 5000;
+ Phaser startOfRound = new Phaser(2);
+ FutureTask<Void> reader = new FutureTask<>(() -> {
+ try {
+ for (int i = 0; i < rounds; i++) {
+ startOfRound.arriveAndAwaitAdvance();
+ long rows = meta.getRowCountWithDeltaRows(table,
BASE_INDEX_ID);
+ Assertions.assertEquals(150L, rows, "row count at round "
+ i);
}
+ } finally {
+ startOfRound.arriveAndDeregister();
}
+ return null;
});
- reader.start();
- // Only transitions which really happen are performed: an analysis
collecting the 100 rows the table
- // had, while 150 rows are loaded. A reader which pairs the collected
row count of one state with the
- // baseline of the other would compute 100 + (150 - 0) = 250.
- for (int i = 0; i < 5000; i++) {
- meta.update(analyzeJob(), table);
- meta.updatedRows.set(150);
+ new Thread(reader, "table-stats-snapshot-reader").start();
+ try {
+ for (int i = 0; i < rounds; i++) {
+ startOfRound.arriveAndAwaitAdvance();
+ meta.update(i % 2 == 0 ? second : first, table);
+ }
+ } finally {
+ startOfRound.arriveAndDeregister();
}
- stop.set(true);
- reader.join();
- Assertions.assertFalse(incoherent.get(), "the reader saw a row count
of neither state");
+ reader.get();
+ Assertions.assertEquals(0,
Mockito.mockingDetails(table).getInvocations().size(),
+ "the table mock retained snapshot test calls");
}
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]