github-actions[bot] commented on code in PR #68282:
URL: https://github.com/apache/doris/pull/68282#discussion_r4067844413


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java:
##########
@@ -3903,8 +3903,18 @@ public void truncateTable(String dbName, String 
tableName, PartitionNamesInfo pa
             long versionTimeMs = Config.isNotCloudMode() ? 
System.currentTimeMillis() : 0L;
             oldPartitions = truncateTableInternal(olapTable, newPartitions,
                     truncateEntireTable, recyclePartitionParamMap, forceDrop, 
version, versionTimeMs);
+            // Transactions which started not later than this watermark were 
removed by the truncation, their
+            // row count updates may still arrive and must not be counted as 
rows loaded after it. Reading the
+            // watermark only protects against those stale updates, so it must 
not fail the truncation.
+            long truncateTxnId = -1;
+            try {
+                truncateTxnId = 
Env.getCurrentEnv().getGlobalTransactionMgr().getTransactionIdWatermark();
+            } catch (UserException e) {

Review Comment:
   The watermark is read only after `truncateTableInternal` has replaced the 
partitions, while transaction admission does not take this table lock. A load 
can begin on the new partition incarnation before this read; its ID is included 
in `truncateTxnId`, so `isUpdateOfTruncatedRows(txnId)` drops its valid 
post-truncate delta. Please establish the transaction/generation boundary 
atomically with replacement (or carry partition incarnation into the update 
event).



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java:
##########
@@ -3903,8 +3903,18 @@ public void truncateTable(String dbName, String 
tableName, PartitionNamesInfo pa
             long versionTimeMs = Config.isNotCloudMode() ? 
System.currentTimeMillis() : 0L;
             oldPartitions = truncateTableInternal(olapTable, newPartitions,
                     truncateEntireTable, recyclePartitionParamMap, forceDrop, 
version, versionTimeMs);
+            // Transactions which started not later than this watermark were 
removed by the truncation, their
+            // row count updates may still arrive and must not be counted as 
rows loaded after it. Reading the
+            // watermark only protects against those stale updates, so it must 
not fail the truncation.
+            long truncateTxnId = -1;
+            try {
+                truncateTxnId = 
Env.getCurrentEnv().getGlobalTransactionMgr().getTransactionIdWatermark();
+            } catch (UserException e) {
+                LOG.warn("Failed to get the transaction id watermark of 
truncate table {}.{}",
+                        db.getFullName(), olapTable.getName(), e);

Review Comment:
   If MetaService cannot provide the watermark, this catch leaves 
`truncateTxnId = -1` but still commits the retained stats record. That sentinel 
makes every delayed transaction update pass the fence, so a pre-truncate load 
can repopulate removed rows. Please fail/defer the whole-table truncate or 
persist a fail-closed generation state; do not proceed with an unfenced reset.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/TableStatsMeta.java:
##########
@@ -195,6 +273,15 @@ public void update(AnalysisInfo analyzedJob, TableIf 
tableIf) {
             if (tableIf instanceof OlapTable) {
                 OlapTable olapTable = (OlapTable) tableIf;
                 indexesRowCount.putAll(analyzedJob.indexesRowCount);
+                // The collected row count above already includes the rows 
which had been loaded when the
+                // job was built, remember how many they were, they are not 
delta rows. The baseline may
+                // only advance together with the collected base index row 
count, an analysis of another
+                // index (a materialized view) doesn't touch it.
+                // Statistics supplied by the user are not collected from the 
table, they carry no baseline.
+                if (!analyzedJob.userInject
+                        && 
analyzedJob.indexesRowCount.containsKey(olapTable.getBaseIndexId())) {
+                    updatedRowsBase.set(analyzedJob.updateRows);
+                }

Review Comment:
   A job created after a prior analysis but before any load carries `updateRows 
= 0` while its collected row count still describes the old 100 rows. If it 
completes after reset, `updatedRowsBase` becomes 0 and the old 100 is restored; 
with five new rows, fallback returns 105 instead of 5. Please carry the 
truncate generation into analysis jobs and discard pre-reset completions.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1356,7 +1356,7 @@ public void replayUpdateRowsRecord(UpdateRowsEvent event) 
{
         if (event.getRecords() != null) {
             for (Entry<Long, Long> record : event.getRecords().entrySet()) {
                 TableStatsMeta statsStatus = idToTblStats.get(record.getKey());
-                if (statsStatus != null) {
+                if (statsStatus != null && 
!statsStatus.isUpdateOfTruncatedRows(event.getTxnId())) {
                     statsStatus.updatedRows.addAndGet(record.getValue());

Review Comment:
   `event.getTxnId()` is the parent transaction ID, but a multi-table 
transaction can add this table after its truncate. Those rows target the new 
partition incarnation yet `txnId <= truncateTxnId` causes them to be discarded. 
Please carry the per-table subtransaction/write generation or partition 
incarnation into the event instead of using only the parent ID.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java:
##########
@@ -3903,8 +3903,18 @@ public void truncateTable(String dbName, String 
tableName, PartitionNamesInfo pa
             long versionTimeMs = Config.isNotCloudMode() ? 
System.currentTimeMillis() : 0L;
             oldPartitions = truncateTableInternal(olapTable, newPartitions,
                     truncateEntireTable, recyclePartitionParamMap, forceDrop, 
version, versionTimeMs);
+            // Transactions which started not later than this watermark were 
removed by the truncation, their
+            // row count updates may still arrive and must not be counted as 
rows loaded after it. Reading the
+            // watermark only protects against those stale updates, so it must 
not fail the truncation.
+            long truncateTxnId = -1;
+            try {
+                truncateTxnId = 
Env.getCurrentEnv().getGlobalTransactionMgr().getTransactionIdWatermark();
+            } catch (UserException e) {

Review Comment:
   This call reaches MetaService synchronously while the table write lock 
acquired above remains held. A slow/unavailable MetaService can stall readers, 
loads, and DDL for the RPC timeout/retry duration. Please obtain a transaction 
barrier outside the table lock and coordinate it with replacement instead of 
performing external I/O in this critical section.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1487,6 +1487,29 @@ public void removeTableStats(long tableId) {
         }
     }
 
+    /**
+     * TRUNCATE TABLE removes all the data of the table, but the table itself 
stays and can be loaded
+     * immediately. The stats record must be kept, otherwise the row count of 
the newly loaded data can
+     * never be reported: the backends report the row count of the new empty 
tablets with a delay of up to
+     * {@code tablet_stat_update_interval_second}, and without a record there 
is nothing to accumulate the
+     * loaded rows into. So reset the record to the state of an empty table 
instead of removing it.
+     *
+     * <p>The transition is journaled by the truncate itself: the master DDL 
path and
+     * {@link InternalCatalog#replayTruncateTable} both apply it. It must not 
be journaled here, a separate
+     * entry is not atomic with the truncate entry, and a crash in between 
would replay a zeroed record
+     * onto the data which was never truncated.
+     */
+    public void resetTableStats(OlapTable table, long truncateTxnId) {
+        synchronized (idToTblStats) {
+            TableStatsMeta tableStats = idToTblStats.get(table.getId());
+            if (tableStats == null) {
+                tableStats = new TableStatsMeta(table);
+                idToTblStats.put(table.getId(), tableStats);

Review Comment:
   `updateTableStats` can observe no record before TRUNCATE, then publish `new 
TableStatsMeta(...)` after `resetTableStats` has created the fenced record. 
That replacement defaults `truncateTxnId` to `-1`, so delayed 
removed-transaction updates pass the fence and stale state replaces the reset. 
Please make first-record publication atomic with reset/generation checks and 
preserve the current fence.



##########
fe/fe-core/src/main/java/org/apache/doris/cloud/transaction/CloudGlobalTransactionMgr.java:
##########
@@ -545,7 +545,7 @@ public void afterCommitTxnResp(CommitTxnResponse 
commitTxnResponse, List<TabletC
             }
         }
         Env env = Env.getCurrentEnv();
-        env.getAnalysisManager().updateUpdatedRows(updatedRows);
+        env.getAnalysisManager().updateUpdatedRows(updatedRows, txnId);
         // 2. update table and partition version

Review Comment:
   This callback can enqueue `OP_LOG_UPDATE_ROWS` concurrently with truncate. 
If a valid post-truncate event runs after the in-memory reset but before 
`logTruncateTable`, its journal entry may precede `OP_TRUNCATE_TABLE`; a 
follower then replays event-then-reset and loses rows the master retained. 
Please serialize these journal transitions or make replay 
generation-aware/commutative.



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/TableStatsMeta.java:
##########
@@ -130,6 +142,72 @@ public TableStatsMeta(long rowCount, AnalysisInfo 
analyzedJob, TableIf table) {
         update(analyzedJob, table);
     }
 
+    /**
+     * Create a record for a table which doesn't have one yet, in the state of 
an empty table. The rows
+     * loaded into the table are accumulated by {@link 
AnalysisManager#replayUpdateRowsRecord}, so a record
+     * has to exist before the first load, otherwise these rows can never be 
turned into a row count.
+     */
+    public TableStatsMeta(OlapTable table) {
+        this.ctlId = table.getDatabase().getCatalog().getId();
+        this.ctlName = table.getDatabase().getCatalog().getName();
+        this.dbId = table.getDatabase().getId();
+        this.dbName = table.getDatabase().getFullName();
+        this.tblId = table.getId();
+        this.tblName = table.getName();
+        this.idxId = -1;
+        this.indexesRowCount = buildEmptyIndexRowCount(table);
+        this.updatedRowsBase.set(0);
+    }
+
+    /**
+     * TRUNCATE TABLE removes all the data of the table. Reset this record 
back to the state of an empty
+     * table instead of dropping it, so that the rows loaded after the 
truncation can still be accumulated
+     * into {@link #updatedRows} and be reported as the row count of the table.
+     */
+    public void reset(OlapTable table, long truncateTxnId) {
+        rowCount = 0;
+        updatedRows.set(0);
+        // Nothing has been collected for the emptied table, so none of the 
rows loaded from now on is
+        // included in the collected row count. They are all delta rows.

Review Comment:
   `reset()` clears `updatedRows` before it writes `truncateTxnId`, but 
`replayUpdateRowsRecord()` does not take the reset/map lock. A delayed 
pre-truncate event can read the old `-1` in this window, add rows after the 
clear, and then survive because reset does not clear again. Publish the 
generation before clearing or synchronize/reset atomically so no update can 
pass during the transition.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java:
##########
@@ -3903,8 +3903,18 @@ public void truncateTable(String dbName, String 
tableName, PartitionNamesInfo pa
             long versionTimeMs = Config.isNotCloudMode() ? 
System.currentTimeMillis() : 0L;
             oldPartitions = truncateTableInternal(olapTable, newPartitions,
                     truncateEntireTable, recyclePartitionParamMap, forceDrop, 
version, versionTimeMs);
+            // Transactions which started not later than this watermark were 
removed by the truncation, their
+            // row count updates may still arrive and must not be counted as 
rows loaded after it. Reading the
+            // watermark only protects against those stale updates, so it must 
not fail the truncation.
+            long truncateTxnId = -1;
+            try {
+                truncateTxnId = 
Env.getCurrentEnv().getGlobalTransactionMgr().getTransactionIdWatermark();
+            } catch (UserException e) {
+                LOG.warn("Failed to get the transaction id watermark of 
truncate table {}.{}",
+                        db.getFullName(), olapTable.getName(), e);
+            }
             if (truncateEntireTable) {
-                
Env.getCurrentEnv().getAnalysisManager().removeTableStats(olapTable.getId());
+                
Env.getCurrentEnv().getAnalysisManager().resetTableStats(olapTable, 
truncateTxnId);
             } else {

Review Comment:
   The task holds the reset object and ends by assigning `tableStats.rowCount = 
table.getRowCount()`. That value is non-strict (unknown partitions contribute 
zero), while post-truncate rows remain in `updatedRows` with baseline 0. With 
one new partition reported at 3 and another unknown, fallback becomes 6 instead 
of 3. Please fence this cleanup by reset generation and avoid replacing the 
empty baseline with a partial count.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to