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


##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVRelationManager.java:
##########
@@ -486,6 +484,15 @@ private void processBaseTableChange(BaseTableInfo 
baseTableInfo, String msgPrefi
             if (checkIvmQueryUsable) {
                 invalidateIvmBaselineIfQueryUnusable(baseTableInfo, mtmv);
             }
+            if (renamed && mtmv instanceof MTMV && ((MTMV) mtmv).isIvm()) {
+                // A rename leaves every column alone, and the failure it does 
cause -- the MV query still
+                // spells the old name -- is reported by the refresh itself: 
it resolves the base tables from
+                // the query (MTMVTask#run) before it looks at anything else, 
so the state is not what makes
+                // that failure visible. What the state does to an IVM MV is 
make the next refresh rebuild the
+                // whole MV (MTMVTask#buildAttempts), which a rename back 
would have it repeat for nothing. A
+                // non-IVM MV keeps the state it has always got, which is what 
its own refresh reads.
+                continue;

Review Comment:
   [P1] Preserve the dependency mapping across this rename. BaseTableInfo 
equality is name-based, but this branch neither invalidates the IVM nor re-keys 
tableMTMVsOneLevelAndFromView from oldTableInfo to newTableInfo. After t is 
renamed to tmp, metadata-only DDL such as TRUNCATE on tmp therefore finds no 
dependent MV; renaming tmp back also looks up only tmp and misses. The query is 
usable again, yet no dirty epoch exists and the delta stream cannot remove the 
truncated rows. Please move/alias the dependency entry on rename or keep a 
conservative invalidation, with a rename -> TRUNCATE -> rename-back regression.



##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPartitionState.java:
##########
@@ -61,6 +61,43 @@ public MTMVPartitionState(MTMVPartitionState other) {
         this.latestEpoch = other.latestEpoch;
     }
 
+    /**
+     * The state a partition gets when it is first aligned: never refreshed, 
one generation required.
+     *
+     * <p>Alignment only ever creates this pair, so "no entry yet" and "this 
pair" say the same thing
+     * about the past -- the partition was never marked and holds no rows.
+     */
+    public static MTMVPartitionState initial() {
+        return new MTMVPartitionState(0, 1);
+    }
+
+    /**
+     * Whether this partition holds rows that a metadata-only change of a base 
table has made unusable,
+     * so it has to be rebuilt rather than caught up incrementally.
+     *
+     * <p>{@code latestEpoch > refreshEpoch} alone does not say that: {@code 
refreshEpoch == 0} means the
+     * partition was never refreshed, so it holds no rows and its first 
refresh reads the current base
+     * tables anyway -- it cannot carry in rows that no longer exist. Calling 
that dirty would rebuild
+     * every partition of a fresh MV for nothing.
+     *
+     * <p>The rejected alternative asked the partition itself instead ({@code 
visibleVersion == 1} means
+     * "no data", and a rebuild would put {@code refreshEpoch} back to 0 for 
such a partition). It is
+     * better in one way -- it does not rebuild a partition that is empty 
because its base partitions are
+     * empty -- but it leaks silently as soon as an anti-join, {@code NOT IN} 
or {@code NOT EXISTS}
+     * incremental refresh exists: removing input rows does not always remove 
output rows (a left join
+     * filtered by {@code b.v IS NULL} becomes non-empty when the matching 
rows go away), and that change
+     * emits no row binlog. Betting the criterion on today's operator support 
is exactly what this state
+     * exists to avoid, so the conservative reading wins.
+     */
+    public boolean isDirty() {
+        return latestEpoch > refreshEpoch && refreshEpoch != 0;

Review Comment:
   [P1] Do not equate refreshEpoch == 0 with durable emptiness. Alignment 
journals (0,1) before the first batch, but the MV DML commits before the task 
publishes its epoch/snapshot and before ADD_TASK is journaled. An FE crash in 
that cut (or cancel(true) after the executor callback clears executor but 
before batch publication) leaves committed rows behind replayed (0,1). A later 
TRUNCATE raises it to (0,2), this returns false, and strict incremental can 
accept an empty delta and mark the stale rows clean. Persist an 
in-progress/possibly-populated state before writing, or otherwise make batch 
commit and durable epoch publication recover as one lifecycle.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java:
##########
@@ -342,7 +347,14 @@ public boolean addTaskResult(AlterMTMV alterMTMV, boolean 
isReplay) {
             }
             this.jobInfo.addHistoryTask(task);
             compatiblePctSnapshot(partitionSnapshots);
-            this.refreshSnapshot.updateSnapshots(partitionSnapshots, 
getPartitionNames());
+            // What this task wrote is described by the epochs just recorded, 
so a partition the result
+            // left dirty is left out: its snapshot would otherwise come back 
after an invalidation
+            // dropped it, and transparent rewrite reads that map to decide 
what it may serve.
+            Map<String, MTMVRefreshPartitionSnapshot> snapshotsToWrite = 
partitionSnapshots;
+            if (!isReplay && ivmInfo.isEnableIvm()) {
+                snapshotsToWrite = 
snapshotsOfCleanPartitions(partitionSnapshots);

Review Comment:
   [P2] Journal the same filtered snapshot map that is applied live. When an 
invalidation lands during a task, the leader deliberately omits that dirty 
partition here, but alterMTMV still contains the raw task map. Replay restores 
the journaled dirty states, skips this !isReplay filter, and re-adds that 
snapshot, so restart does not reconstruct the leader's state or preserve the 
documented removal invariant. With all snapshot-bearing partitions dirty this 
also changes attempt selection from the no-snapshot COMPLETE branch (rebuilt 
count 0) to the all-dirty COMPLETE branch (count N). Put a detached 
snapshotsToWrite on the payload before submitAlterLog and cover 
invalidation-before-ADD_TASK replay order.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -768,6 +749,9 @@ private IvmIncrRefreshResult 
executeSingleIvmAttempt(MTMVRefreshContext refreshC
         } catch (Exception e) {
             throw new JobException("IVM snapshot generation failed for mv=" + 
mtmv.getName(), e);
         }
+        // The requirement these partitions are read under, captured before 
the read inside doRefresh and
+        // recorded only if the refresh commits; see captureLatestEpochs.
+        Map<String, Long> capturedEpochs = 
captureLatestEpochs(Sets.newHashSet(needRefreshPartitions));

Review Comment:
   [P1] Recheck invalidation after choosing the rebuild set. A TRUNCATE can 
raise this partition's epoch and remove its snapshot after executeIvmAttempt 
sampled dirtyPartitions. It then enters incrementalScope, this call captures 
the raised epoch, and the row-delta refresh cannot delete the rows removed only 
through metadata. Success consequently writes refreshEpoch == latestEpoch and 
leaves those old MV rows permanently clean. Dirty selection and epoch capture 
need one generation decision (or an epoch advance here must abort/reroute the 
partition to rebuild); please add a latch test for the sample -> TRUNCATE -> 
capture ordering.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -312,15 +321,20 @@ public void run() throws JobException {
             // refresh fallback: incompatible MV definitions must fail 
directly.
             ensureQueryUsableIfNeeded(ctx, tableIfs);
             RefreshRequest request = resolveRefreshRequest();
-            validateIvmBaselineBeforePartitionSync(request);
-            List<RefreshAttemptType> attempts = buildAttempts(request, 
queryAnalysis.containsOneRowRelation());
             try {
                 syncPartitionsIfNeeded(ctx, tableIfs);
             } catch (PartitionPlanningException e) {
                 throw new JobException(e.getMessage(), e);
             }
+            // Partition sync has decided which partitions exist, and nothing 
has read a base table yet:
+            // this is the point where an entry and the partition it describes 
become the same thing.
+            // Doing it any later would let a partition that sync has just 
added be refreshed without an
+            // entry, and an invalidation arriving in between would have 
nothing to land on.
+            mtmv.alignPartitionStates(mtmv.getPartitionNames());

Review Comment:
   [P1] Avoid making every later ADD_TASK carry this full aligned map. The 
existing task-result path deep-copies partitionStates under mvRwLock and 
serializes the whole map as JSON, but before this change the production map was 
never populated. This line now creates one entry per MV partition, so even a 
no-op scheduled refresh or a task advancing one partition emits O(total 
partitions) state. Doris already exercises 160,000 mapped MV partitions, making 
each periodic result a multi-megabyte record and lock-held copy. Persist only 
the task's detached epoch delta on ADD_TASK (and omit it when empty), keeping 
full maps for alignment/invalidation records.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -448,6 +462,17 @@ private List<RefreshAttemptType> 
buildAttempts(RefreshRequest request, boolean c
         if (shouldUseCompleteForInitialIvmRefresh(containsOneRowRelation)) {
             return Lists.newArrayList(RefreshAttemptType.COMPLETE);
         }
+        // A schema-level invalidation is not a set of dirty partitions: it 
means every partition, including
+        // the ones partition sync has not created yet, and no per-partition 
requirement can express that.
+        // IVM only -- a non-IVM MV reaches the same effect through its 
cleared snapshot, which its own
+        // refresh already depends on.
+        if (mtmv.isIvm() && !request.explicitPartitions
+                && mtmv.getStatus().getState() == MTMVState.SCHEMA_CHANGE) {
+            LOG.info("IVM MV is in SCHEMA_CHANGE, rebuilding the whole MV, 
mv={}, taskId={}",
+                    mtmv.getName(), getTaskId());
+            recordRebuiltPartitions(request, mtmv.getPartitionNames().size());
+            return Lists.newArrayList(RefreshAttemptType.COMPLETE);

Review Comment:
   [P2] Publish the rebuilt plan signature on this direct COMPLETE path. 
SCHEMA_CHANGE can come from altering a base view while keeping the MV output 
schema valid but changing its normalized join/layout plan. Because this branch 
skips the incremental attempt, ivmFallbackReason is never 
PLAN_SIGNATURE_MISMATCH; executePartitionBasedRefresh discards the signature 
produced by this successful rebuild and ADD_TASK keeps the old one. The next 
AUTO refresh then performs a second COMPLETE through mismatch fallback, while a 
next strict INCREMENTAL rejects the baseline it just rebuilt. Capture/persist 
the consistent full-refresh signature when this branch establishes the new 
baseline.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -715,13 +664,36 @@ private AttemptResultType 
executeIvmAttempt(MTMVRefreshContext refreshContext,
                     + "Continuing with COMPLETE refresh.", mtmv.getName(), 
getTaskId());
             return AttemptResultType.FALLBACK_TO_COMPLETE;
         }
+        // The partitions the criterion says must be rebuilt rather than 
caught up: the delta path can only
+        // append, so a partition it treated as current would record that in 
its epoch while its rows still
+        // come from before the change. Rebuilt first, with the partition 
executor, because that is the
+        // full recomputation they need -- and only in this task's batches, so 
a change that arrives while
+        // it runs leaves them dirty for the next round instead of being 
swallowed.
+        Set<String> dirtyPartitions = mtmv.getDirtyPartitions();
+        Map<String, MTMVRefreshPartitionSnapshot> rebuiltSnapshots = 
Maps.newHashMap();
+        if (!dirtyPartitions.isEmpty()) {
+            LOG.info("Rebuilding {} invalidated MV partitions before the 
incremental refresh, mv={}, taskId={}",
+                    dirtyPartitions.size(), mtmv.getName(), getTaskId());
+            List<String> toRebuild = Lists.newArrayList(dirtyPartitions);
+            toRebuild.sort(Comparator.naturalOrder());
+            this.needRefreshPartitions = toRebuild;
+            this.refreshMode = generateRefreshMode(toRebuild);
+            executePartitionBasedRefresh(refreshContext, 
RefreshMode.PARTITIONS, ctx);
+            rebuiltSnapshots.putAll(partitionSnapshots);
+            recordRebuiltPartitions(request, dirtyPartitions.size());

Review Comment:
   [P2] Count rebuilt partitions as batches commit. If an early dirty-rebuild 
group commits and a later group fails, this line is never reached and 
IvmRebuiltPartitions stays 0 even though ADD_TASK preserves the committed 
group's epochs/data. The COMPLETE escalation sites have the inverse problem: 
they record every planned partition before the first batch, so an immediate 
failure reports all rebuilt. The new diagnostic should reflect successful 
groups, especially on failed tasks.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -715,13 +664,36 @@ private AttemptResultType 
executeIvmAttempt(MTMVRefreshContext refreshContext,
                     + "Continuing with COMPLETE refresh.", mtmv.getName(), 
getTaskId());
             return AttemptResultType.FALLBACK_TO_COMPLETE;
         }
+        // The partitions the criterion says must be rebuilt rather than 
caught up: the delta path can only
+        // append, so a partition it treated as current would record that in 
its epoch while its rows still
+        // come from before the change. Rebuilt first, with the partition 
executor, because that is the
+        // full recomputation they need -- and only in this task's batches, so 
a change that arrives while
+        // it runs leaves them dirty for the next round instead of being 
swallowed.
+        Set<String> dirtyPartitions = mtmv.getDirtyPartitions();
+        Map<String, MTMVRefreshPartitionSnapshot> rebuiltSnapshots = 
Maps.newHashMap();
+        if (!dirtyPartitions.isEmpty()) {
+            LOG.info("Rebuilding {} invalidated MV partitions before the 
incremental refresh, mv={}, taskId={}",
+                    dirtyPartitions.size(), mtmv.getName(), getTaskId());
+            List<String> toRebuild = Lists.newArrayList(dirtyPartitions);
+            toRebuild.sort(Comparator.naturalOrder());
+            this.needRefreshPartitions = toRebuild;
+            this.refreshMode = generateRefreshMode(toRebuild);
+            executePartitionBasedRefresh(refreshContext, 
RefreshMode.PARTITIONS, ctx);
+            rebuiltSnapshots.putAll(partitionSnapshots);
+            recordRebuiltPartitions(request, dirtyPartitions.size());
+        }
         MTMVRefreshContext currentRefreshContext = refreshContext;
         int ivmAttemptLimit = Math.max(Config.max_query_retry_time, 0) + 1;
         IvmIncrRefreshResult ivmResult = null;
         for (int partitionSyncRetryCount = 0;
                 partitionSyncRetryCount < ivmAttemptLimit; 
partitionSyncRetryCount++) {
-            ivmResult = executeSingleIvmAttempt(currentRefreshContext);
+            ivmResult = executeSingleIvmAttempt(currentRefreshContext, 
dirtyPartitions);
             if (ivmResult.isSuccess()) {
+                // The incremental attempt reset the accumulators it owns, so 
the rebuild's are merged back
+                // here: its batches committed, and without them the 
partitions it rebuilt would look
+                // unsynced and be refreshed again on every following round.
+                this.partitionSnapshots.putAll(rebuiltSnapshots);
+                this.completedPartitions.addAll(dirtyPartitions);

Review Comment:
   [P2] Keep the progress denominator consistent when merging the rebuild 
phase. executeSingleIvmAttempt resets needRefreshPartitions to only the 
incremental scope and clears completedPartitions; this line then adds the dirty 
rebuilds only to the completed side. A task with one rebuilt and one 
incremental partition is recorded as 200% (2/1), while a dirty-only success 
reports null progress because the denominator is empty. Preserve the union of 
both phase scopes (and their completed sets) for task history.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -448,6 +462,17 @@ private List<RefreshAttemptType> 
buildAttempts(RefreshRequest request, boolean c
         if (shouldUseCompleteForInitialIvmRefresh(containsOneRowRelation)) {
             return Lists.newArrayList(RefreshAttemptType.COMPLETE);
         }
+        // A schema-level invalidation is not a set of dirty partitions: it 
means every partition, including
+        // the ones partition sync has not created yet, and no per-partition 
requirement can express that.
+        // IVM only -- a non-IVM MV reaches the same effect through its 
cleared snapshot, which its own
+        // refresh already depends on.
+        if (mtmv.isIvm() && !request.explicitPartitions

Review Comment:
   [P2] Distinguish whole-IVM invalidation from generic SCHEMA_CHANGE here. 
processBaseTableChange sets this state even when re-analysis succeeds and no 
partition epoch was raised (for example DROP COLUMN spare, which the changed 
test says must not invalidate the baseline). This branch nevertheless converts 
the following strict INCREMENTAL request into a whole COMPLETE rebuild; the 
test checks only SUCCESS, so it misses that route. Use a dedicated 
whole-baseline marker or avoid setting this state for compatible IVM changes, 
and assert RefreshMode/IvmRebuiltPartitions.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/mtmv/MTMVTask.java:
##########
@@ -567,32 +633,13 @@ private MTMVRefreshContext 
buildRefreshContext(List<TableIf> tableIfs) throws An
         }
     }
 
-    /**
-     * Makes the barrier that says "these MV partitions must be rebuilt before 
their IVM offsets may be
-     * used again" durable. Every caller writes it as soon as it has decided 
the partition set and
-     * before anything that touches MV data or base table streams, so that a 
crash or a rejection can
-     * only ever leave a barrier with no rebuild behind it, which merely costs 
one extra rebuild, and
-     * never a rebuild with no barrier, which silently loses rows.
-     */
-    private void writeIvmBaselineBarrier(RefreshMode refreshMode) throws 
JobException {
-        if (mtmv.isIvm()) {
-            // Persist the guard before the first baseline data transaction.
-            mtmv.persistIvmBaselineGuard(refreshMode, 
Sets.newHashSet(needRefreshPartitions),
-                    mtmvSchemaChangeVersion);
-        }
-    }
-
     private void executeCompleteAttempt(MTMVRefreshContext context, 
ConnectContext ctx)
             throws JobException, AnalysisException {
         this.needRefreshPartitions = 
Lists.newArrayList(mtmv.getPartitionNames());
         this.refreshMode = generateRefreshMode(needRefreshPartitions);
         if (refreshMode == MTMVTaskRefreshMode.NOT_REFRESH) {
             return;
         }
-        // The barrier goes first: a stream this rebuild reconciles carries 
the base table's current
-        // rows as its initial snapshot, and a later incremental refresh that 
consumed it as a delta
-        // against data still built from the old baseline would double-count 
them.
-        writeIvmBaselineBarrier(RefreshMode.COMPLETE);

Review Comment:
   [P1] Keep a durable rebuild requirement before recreating an IVM stream. A 
dropped/unusable stream sends a fallback refresh to COMPLETE, but 
reconcileIvmStreams durably creates its replacement with show_initial_rows=true 
before any MV rebuild batch. If FE crashes after that create and before the 
rebuild/ADD_TASK, restart has the old populated MV with clean nonzero epochs 
plus a usable stream whose historical rows are exposed as APPEND; the next 
AUTO/INCREMENTAL refresh can therefore add those rows to the old baseline 
again. This removed barrier protected exactly that cut. Persist a 
whole-MV/affected-partition requirement before reconciliation and clear it only 
after the rebuilt baseline is durably published, with a failover test at this 
boundary.



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