yujun777 commented on PR #69028:
URL: https://github.com/apache/doris/pull/69028#issuecomment-6463338945

   ### TRUNCATE removes the stats record without journaling the deletion, so 
the record a load creates now diverges between the master and its followers
   
   This is the one removal site that is not ordered with a journal entry, and 
it is the scenario this PR is about.
   
   **The gap.** A whole table `TRUNCATE` drops the record in memory only — 
`InternalCatalog.java:3930` calls 
`AnalysisManager.removeTableStats(olapTable.getId())` and never calls 
`logDeleteTableStats`, and `InternalCatalog.replayTruncateTable` 
(`InternalCatalog.java:3998`) only replaces the partitions and fixes up the 
`TabletInvertedIndex`, so a follower replaying `OP_TRUNCATE_TABLE` keeps the 
record. The other removals this PR orders are all paired with something the 
followers replay: a whole table `DROP STATS` journals `DELETE_TABLE_STATS` (now 
via `removeTableStatsAndLog`), and a schema change is paired with the 
`FINISHED` entry through `EditLog.java:972`. `TRUNCATE` has no counterpart at 
all.
   
   **Why it becomes a defect here.** Before this change a follower could only 
ever hold a record that an analyze had produced, so an unjournaled removal 
merely left both sides with different *contents* of an old record. Now 
`replayUpdateRowsRecord` creates the record of a table that has none, and the 
rows it accumulates are journaled: `TransactionState.tableIdToTabletDeltaRows` 
is `@SerializedName("deltaRows")` (`TransactionState.java:289`), so 
`replayUpsertTransactionState` -> `updateCatalogAfterVisible` recreates the 
record on every follower as well. The master's record is therefore no longer 
the one the followers have.
   
   For a table that was never analyzed, following the reproduction in the PR 
description:
   
   | step | master | follower |
   | --- | --- | --- |
   | load 50,000,000 rows | record created: `rowCount=0`, `updatedRowsBase=0`, 
`updatedRows=5e7` | same (replayed) |
   | `TRUNCATE TABLE t` | record removed, **nothing journaled** | record kept, 
`updatedRows=5e7` |
   | load 3 rows | record recreated: `updatedRows=3` -> scan at 3 | accumulates 
-> `updatedRows=50000003` -> scan at 50,000,003 |
   
   Before this PR both frontends agreed on the (wrong) one-row estimate for 
such a table; now the master is right and every follower plans a three-row 
table as if it still held the rows the truncation removed.
   
   **Impact.** A follower coordinates and plans queries on its own, so in a 
multi-FE cluster the same statement gets a different join order and a different 
broadcast/shuffle choice depending on which frontend it lands on, for up to 
`tablet_stat_update_interval_second` while the new tablets are unreported. The 
residue outlives that window: `updatedRows` is only rebased by an analyze, so 
the follower keeps reporting the pre-truncate counter as 
`Statistics.deltaRowCount`, which `FilterEstimation.java:94` uses as the 
population to retry a zero estimate with, until a whole table `ANALYZE` 
converges the two sides (`replayUpdateTableStatsStatus` replaces the record). 
If the follower is promoted, its divergent record becomes authoritative. Row 
visibility and delete bitmaps are unaffected — this is a cardinality-only 
defect.
   
   **Suggested fix.** `truncateTable` already holds the table write lock from 
before the removal until after `logTruncateTable`, which is exactly the 
critical section this PR requires a removal to live in, so it should go through 
the new helper:
   
   ```java
   // InternalCatalog.truncateTable, the truncateEntireTable branch
   Env.getCurrentEnv().getAnalysisManager().removeTableStatsAndLog(olapTable);
   ```
   
   The `writeLock()` inside the helper is reentrant, and a follower replaying 
`DELETE_TABLE_STATS` for a record it does not have is a no-op `remove`. The 
journal order then becomes `[VISIBLE create] -> [DELETE] -> [VISIBLE create]`, 
which matches the order the master applies them in.
   
   For coverage, 
`AnalysisManagerTest#testCloudRowUpdateIsSerializedWithTheTableLock` is the 
shape to copy — assert the removal and its journal entry appear paired on the 
truncate path. `Alter.java:858` looks like another removal of this class (the 
original table of an alter that does not swap) and may be worth checking in the 
same pass.
   


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