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]

Reply via email to