englefly commented on code in PR #68282:
URL: https://github.com/apache/doris/pull/68282#discussion_r4071360669


##########
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:
   Withdrawn in 343cd946a4a together with the fence, and the point is one of 
the reasons for it.
   
   Confirmed: the watermark was read after `truncateTableInternal()` had 
replaced the partitions
   (`InternalCatalog`, the read was right after the call), and transaction 
admission does not take the table
   write lock, so a load admitted between the replacement and the read is 
inside the fence while its rows
   belong to the new partitions. Moving the read before the replacement only 
moves the error to the other
   side: a transaction which started before the replacement and was removed 
would then be counted. The two
   moments (the transaction admission point and the partition replacement 
point) cannot be ordered by
   reading a watermark on either side of the replacement, which is why I 
removed the fence instead of
   retrying the ordering. Agreed that the boundary has to be carried by the 
data itself (partition
   incarnation or a per table write generation).



##########
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:
   Withdrawn in 343cd946a4a, together with the fail-open path.
   
   Confirmed and it is the reason I did not try to make the failure "closed": 
failing a user `TRUNCATE
   TABLE` because the statistics bookkeeping cannot read a watermark is not 
acceptable either, and
   persisting a fail-closed generation state needs the generation to exist in 
the first place. The
   `try/catch` with `truncateTxnId = -1` is gone with the fence, so there is no 
longer a state where the
   code claims to be fenced but is not.



##########
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:
   Withdrawn in 343cd946a4a. Confirmed, and this is the strongest of the nine 
objections.
   
   In cloud mode `CloudGlobalTransactionMgr.getTransactionIdWatermark()` (line 
2681) is
   `getNextTransactionId() + 1`, i.e. it allocates a transaction id through the 
MetaService, so the fence did
   external I/O, with a write side effect, while `olapTable.writeLock()` was 
held by `truncateTable`
   (`fe/fe-core/AGENTS.md` asks metadata-locking paths to avoid RPC and 
external IO under catalog/database/
   table locks). Moving the call outside the lock would not fix the boundary 
problem of the first comment,
   so the fence is removed rather than relocated.



##########
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:
   Withdrawn in 343cd946a4a. Confirmed.
   
   The event carried the parent transaction id and `isUpdateOfTruncatedRows()` 
compared it with the
   watermark, so any transaction which was admitted before the truncation but 
wrote this table afterwards,
   for instance a multi-statement transaction which does `TRUNCATE`-like work 
between its statements, or a
   transaction which adds this table later, had its valid rows discarded. The 
id of the transaction is not
   the right identity for "which incarnation of the table did these rows go 
to", which is exactly what the
   partition/tablet information in the event would give.



##########
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:
   Withdrawn in 343cd946a4a, but the ordering hazard is worth separating from 
the fence.
   
   The master applies the cloud update in memory (`replayUpdateRowsRecord`) and 
then journals it
   (`logUpdateRowsRecord`), while the truncate applies its reset under the 
table write lock and journals the
   truncate entry — the two are not mutually exclusive, so the journal order 
can be the opposite of the
   apply order and a follower then replays reset-then-update or 
update-then-reset. The fence did not address
   that at all. Removing the fence does not introduce it either: it exists as 
soon as the record survives
   the truncation, which is the point of this PR, and it is the same "the 
update describes rows which the
   truncation removed" gap that I have accepted and documented in the commit 
message of 343cd946a4a. A
   generation carried by the event would also have to be part of the journal 
entry to make replay
   commutative, which is the follow-up I proposed.



##########
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:
   This one is not caused by the fence or by this PR; the numbers are the same 
without it.
   
   A job created after an analysis and before any load has `updateRows = 0`, 
and it records that value for
   every column it collected, so the baseline it publishes is 0 under both 
implementations:
   
   * before this PR: `getBaseIndexDeltaRowCount()` took the maximum 
`updatedRows` of the base index column
     statistics, and the stale job sets `colStatsMeta.updatedRows = 
analyzedJob.updateRows = 0`, so the
     baseline is 0 as well, and the fallback is `100 + (5 - 0) = 105`;
   * with the baseline field: `updatedRowsBase.set(0)` and the fallback is `100 
+ (5 - 0) = 105`.
   
   So a stale snapshot which completes after the table was truncated 
over-estimates in the same way before
   and after this change. Fencing it needs the truncate generation to travel 
with the analysis job: a field
   in `AnalysisInfo` (it is journaled and replayed, so it has to be compatible) 
filled in
   `buildAnalysisJobInfo()`, compared in 
`updateTableStats()`/`updateTableStatsForAlterStats()` against the
   generation in the record, and the job dropped when it is older. I am happy 
to do that as a follow-up
   together with the other asynchronous producers (the `DropStatsTask`/analyze 
thread 4063317611), but it is
   a separate change from this PR.



##########
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:
   I looked at this one and I believe the fallback cannot be affected by that 
assignment.
   
   `invalidateLocalStats()` (the method `DropStatsTask` ends with) writes only 
these to the record:
   
   ```java
   if (allPartition) { ... tableStats.removeColumn(indexName, column); }   // 
the column statistics metas
   ...
   tableStats.userInjected = false;
   tableStats.rowCount = table.getRowCount();
   ```
   
   `TableStatsMeta.rowCount` (the field) is not read by the planner fallback: 
it is displayed by
   `SHOW TABLE STATS` (`ShowTableStatsCommand` line ~244) and set for external 
tables, while
   `StatsCalculator.getOlapTableRowCount()` uses 
`tableStats.getRowCount(indexId)` (which reads
   `indexesRowCount`) plus the delta, and `invalidateLocalStats()` never writes 
`indexesRowCount` or
   `updatedRows`. So the fallback cannot become 6 through this path.
   
   The concern behind it, a late asynchronous cleanup applying to a record 
which changed in between, is the
   open thread 4063317611 and I agree it should be made generation-aware; with 
the fence withdrawn there is
   no `truncateTxnId` left to compare, so it needs the same generation 
mechanism as the previous comment
   rather than a change here.



-- 
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