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


##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1298,12 +1344,12 @@ public void updateUpdatedRows(Map<Long, Long> 
partitionToUpdateRows, long dbId,
     }
 
     // Invoke this for cloud version load.
-    public void updateUpdatedRows(Map<Long, Long> updatedRows) {
+    public void updateUpdatedRows(Map<Long, Long> updatedRows, long dbId) {
         try {
             if (!Env.getCurrentEnv().isMaster() || Env.isCheckpointThread()) {
                 return;
             }
-            UpdateRowsEvent updateRowsEvent = new UpdateRowsEvent(updatedRows);
+            UpdateRowsEvent updateRowsEvent = new UpdateRowsEvent(dbId, 
updatedRows);

Review Comment:
   Fixed in 50074783a42.
   
   `AnalysisManager.updateUpdatedRows(Map, dbId)` — the cloud report path — now 
holds the table write
   lock of the reported tables while it applies **and** journals the rows, i.e. 
the same lock which a
   whole table `DROP STATS` takes to remove the record and journal the 
deletion. The journal order is
   then always the order in which the mutations are applied, so both 
interleavings converge:
   
   - report first: journal `[UPDATE_ROWS][DELETE]` -> master create(N) then 
remove, replay create(N) then remove
   - DROP first:   journal `[DELETE][UPDATE_ROWS]` -> master remove then 
create(N), replay remove then create(N)
   
   The tables are locked in id order with the 10 second timeout 
`finishTransaction` uses; when the lock
   cannot be acquired the report is dropped with a warning and the row count 
stays unknown until the
   backends report it.
   
   Test: `AnalysisManagerTest#testCloudRowUpdateIsSerializedWithTheTableLock` 
holds the table write lock,
   reports rows from another thread and asserts that neither the record nor the 
journal entry appears
   while the lock is held (and that both appear after the unlock). It fails 
without the change with
   "the report was applied while the table was locked".
   



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1355,7 +1401,7 @@ public void replayUpdateRowsRecord(UpdateRowsEvent event) 
{
         InternalCatalog catalog = Env.getCurrentInternalCatalog();
         if (event.getRecords() != null) {
             for (Entry<Long, Long> record : event.getRecords().entrySet()) {
-                TableStatsMeta statsStatus = idToTblStats.get(record.getKey());
+                TableStatsMeta statsStatus = findOrCreateTableStats(catalog, 
event.getDbId(), record.getKey());

Review Comment:
   Fixed in d1aca0b7213.
   
   The partition generation of the report is now validated: 
`CloudGlobalTransactionMgr.afterCommitTxnResp`
   hands the `<table_id, partition_id>` pairs which `CommitTxnResponse` already 
carries to
   `AnalysisManager.updateUpdatedRows`, which drops a table's rows when any of 
the partitions the report
   counted no longer belongs to the table (a whole table `TRUNCATE` replaces 
the partitions and keeps the
   table id). The check runs inside the table write lock the report takes, so 
the generation cannot change
   between the check and the application, and a dropped report is neither 
applied nor journaled — so the
   record the next load recreates only counts the rows of the current 
partitions.
   
   Tests: 
`AnalysisManagerTest#testReportOfAPartitionRemovedByTruncateIsNotApplied` (a 
delayed report of
   100 rows for the replaced partition is ignored; the reload's 3 rows recreate 
the record with 3 rows,
   not 103) and `AnalysisManagerTest#testReportOfRemovedPartitionsIsRecognized`.
   



##########
fe/fe-core/src/main/java/org/apache/doris/statistics/analysis/AnalysisManager.java:
##########
@@ -1366,52 +1412,52 @@ public void replayUpdateRowsRecord(UpdateRowsEvent 
event) {
         // Record : TableId -> (TabletId -> update rows)
         if (event.getTabletRecords() != null) {
             for (Entry<Long, Map<Long, Long>> record : 
event.getTabletRecords().entrySet()) {
-                TableStatsMeta statsStatus = idToTblStats.get(record.getKey());
-                if (statsStatus != null) {
-                    Optional<Database> dbOption = 
catalog.getDb(event.getDbId());
-                    if (!dbOption.isPresent()) {
-                        LOG.warn("Database {} does not exist.", 
event.getDbId());
-                        continue;
-                    }
-                    Database database = dbOption.get();
-                    Optional<Table> tableOption = 
database.getTable(record.getKey());
-                    if (!tableOption.isPresent()) {
-                        LOG.warn("Table {} does not exist in DB {}.", 
record.getKey(), event.getDbId());
-                        continue;
-                    }
-                    Table table = tableOption.get();
-                    if (!(table instanceof OlapTable)) {
-                        continue;
-                    }
-                    OlapTable olapTable = (OlapTable) table;
-                    short replicaNum = 
olapTable.getTableProperty().getReplicaAllocation().getTotalReplicaNum();
-                    Map<Long, Long> tabletRows = record.getValue();
-                    if (tabletRows == null || tabletRows.isEmpty()) {
-                        LOG.info("Tablet row count map is empty");
-                        continue;
-                    }
-                    long rowsForAllReplica = 0;
-                    for (Entry<Long, Long> entry : tabletRows.entrySet()) {
-                        rowsForAllReplica += entry.getValue();
-                        if (LOG.isDebugEnabled()) {
-                            LOG.debug("Table id {}, tablet id {}, row count 
{}",
-                                    record.getKey(), entry.getKey(), 
entry.getValue());
-                        }
-                    }
-                    long tableUpdateRows = rowsForAllReplica / replicaNum;
-                    LOG.info("Update rows for table {} is {}, replicaNum is 
{}, "
-                            + "rows for all replica {}, tablets count {}",
-                            olapTable.getName(), tableUpdateRows, replicaNum, 
rowsForAllReplica, tabletRows.size());
-                    statsStatus.updatedRows.addAndGet(tableUpdateRows);
-                    if (StatisticsUtil.enablePartitionAnalyze()) {
-                        updatePartitionRows(olapTable, tabletRows, 
statsStatus, replicaNum);
+                Optional<Database> dbOption = catalog.getDb(event.getDbId());
+                if (!dbOption.isPresent()) {
+                    LOG.warn("Database {} does not exist.", event.getDbId());
+                    continue;
+                }
+                Database database = dbOption.get();
+                Optional<Table> tableOption = 
database.getTable(record.getKey());
+                if (!tableOption.isPresent()) {
+                    LOG.warn("Table {} does not exist in DB {}.", 
record.getKey(), event.getDbId());
+                    continue;
+                }
+                Table table = tableOption.get();
+                if (!(table instanceof OlapTable)) {
+                    continue;
+                }
+                OlapTable olapTable = (OlapTable) table;
+                TableStatsMeta statsStatus = findOrCreateTableStats(olapTable);

Review Comment:
   Fixed in b01c9770473.
   
   The removal moved into the same critical section which journals `FINISHED`, 
i.e. before
   `tbl.writeUnlock()`, so a load can no longer become visible in between: 
either the load ran before the
   schema change finished (the record it updated is removed by the job on both 
the master and the replay)
   or after (the record it recreates exists on both). 
`removeTableStatsOnFinished` asserts that the table
   write lock is held, so the call cannot be moved out of the critical section 
again; dropping the
   persisted statistics and invalidating the caches (`dropStats`) stays outside 
the lock because it does
   not touch the record.
   
   Test: 
`SchemaChangeJobV2StatsTest#testRemoveTableStatsOnFinishedRequiresTheTableWriteLock`
 (the removal
   is rejected without the table write lock and removes the record with it).
   



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