This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 35209ae701c [fix](fe) Let MV dry runs and EXPLAIN REFRESH borrow the 
session's compute group (#70125)
35209ae701c is described below

commit 35209ae701ccd255d4d22eaeac744919331d27e7
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Nov 5 07:28:23 2026 +0000

    [fix](fe) Let MV dry runs and EXPLAIN REFRESH borrow the session's compute 
group (#70125)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #69749, #62606
    
    Problem Summary:
    
    **In short.** In cloud mode, `REFRESH MATERIALIZED VIEW ... INCREMENTAL
    WITH DRY RUN` and `EXPLAIN REFRESH MATERIALIZED VIEW` of an MV that
    declares no `compute_group` plan (and the dry run executes) in whatever
    compute group the `admin` user resolves to, not in the group of the
    session that issues them, where a manual `REFRESH` of the same MV runs.
    This PR binds both to the session's group, the way a manual REFRESH
    borrows it.
    
    **Background.** Both statements build an internal context with the admin
    identity and the MV's session variables
    (`MTMVPlanUtil.createMTMVContext`). Its `cloud_cluster` is empty, so it
    resolves a group the way admin would: admin's `default_compute_group`,
    else the first group admin may use. A manual REFRESH passes the
    session's group to its task instead (`MTMVJobManager#refreshMTMV`), and
    the task binds it when the MV declares none
    (`MTMVTask#applyComputeGroup`). #69749 made both statements honour a
    declared group; an MV that declares none, which is every MV created
    before #69749 and any MV created without the property, still goes to
    admin's group. Found while reviewing #69749 (thread 4401555648).
    
    **The problem, and what it cost.** Measured on master `856194ece0e` with
    a unit test in cloud mode: groups `cg_of_admin` and `cg_of_session`, the
    session on `USE @cg_of_session`, an MV that declares no group.
    
    | | manual REFRESH | dry run | EXPLAIN REFRESH |
    |---|---|---|---|
    | compute group | `cg_of_session` | `cg_of_admin` | `cg_of_admin` |
    
    - If the MV's `workload_group` exists only in the session's group (a
    workload group belongs to one compute group in cloud mode), the dry run
    fails with `Can not find workload group <wg> in compute croup
    cg_of_admin`, while the manual REFRESH succeeds.
    - Otherwise the dry run's delta query runs on the backends of another
    group, which the session's user may have no USAGE on, and starts that
    group if it is suspended. EXPLAIN REFRESH assigns backends there and can
    start it as well. Results are not affected: the dry run only reads, and
    storage is shared.
    
    **How this PR fixes it.**
    
    - The private lookup of the session's group in `MTMVJobManager` moves
    unchanged to `MTMVPlanUtil.getSessionComputeGroup()`, so a manual
    REFRESH, a dry run and EXPLAIN REFRESH resolve it with the same code.
    - `RefreshMTMVCommand` builds both internal contexts through one helper.
    It reads the session's group first, while the session is still the
    thread-local context, then binds the declared group, else the session's
    group, as `MTMVTask#applyComputeGroup` does. With neither, the context
    stays unbound and the existing resolution applies; outside cloud mode
    nothing changes.
    - As in `MTMVTask`, the dry run re-checks only a declared group, since
    the MV never chose the session's group. EXPLAIN REFRESH executes nothing
    and still checks nothing.
    
    **Results.**
    
    | MV | session | dry run / EXPLAIN REFRESH, before | after |
    |---|---|---|---|
    | declares no group | `USE @cg_of_session` | `cg_of_admin` |
    `cg_of_session`, as a manual REFRESH |
    | declares no group | resolves no group | admin's resolution | unchanged
    |
    | declares `cg_declared` | `USE @cg_of_session` | `cg_declared` |
    unchanged |
    | any, outside cloud mode | | no compute group | unchanged |
    
    branch-4.1 and branch-4.2 have neither statement.
---
 .../java/org/apache/doris/mtmv/MTMVJobManager.java | 21 +-----
 .../java/org/apache/doris/mtmv/MTMVPlanUtil.java   | 23 +++++++
 .../trees/plans/commands/RefreshMTMVCommand.java   | 43 ++++++++-----
 .../plans/commands/RefreshMTMVCommandTest.java     | 75 +++++++++++++++++++++-
 4 files changed, 124 insertions(+), 38 deletions(-)

diff --git a/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVJobManager.java 
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVJobManager.java
index 9a23ca479a9..9b09638f818 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVJobManager.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVJobManager.java
@@ -24,8 +24,6 @@ import org.apache.doris.catalog.MTMV;
 import org.apache.doris.catalog.Table;
 import org.apache.doris.catalog.TableIf.TableType;
 import org.apache.doris.catalog.info.TableNameInfo;
-import org.apache.doris.cloud.qe.ComputeGroupException;
-import org.apache.doris.common.Config;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.MetaNotFoundException;
 import org.apache.doris.common.util.TimeUtils;
@@ -45,7 +43,6 @@ import 
org.apache.doris.nereids.trees.plans.commands.info.PauseMTMVInfo;
 import org.apache.doris.nereids.trees.plans.commands.info.RefreshMTMVInfo;
 import 
org.apache.doris.nereids.trees.plans.commands.info.RefreshMTMVInfo.RefreshMode;
 import org.apache.doris.nereids.trees.plans.commands.info.ResumeMTMVInfo;
-import org.apache.doris.qe.ConnectContext;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
@@ -159,26 +156,10 @@ public class MTMVJobManager implements MTMVHookService {
     public void refreshMTMV(RefreshMTMVInfo info) throws DdlException, 
MetaNotFoundException, JobException {
         MTMVJob job = getJobByTableNameInfo(info.getMvName());
         MTMVTaskContext mtmvTaskContext = 
MTMVTaskContext.of(MTMVTaskTriggerMode.MANUAL, info.getPartitions(),
-                info.getRefreshMode(), info.allowFallback(), 
getCurrentComputeGroup());
+                info.getRefreshMode(), info.allowFallback(), 
MTMVPlanUtil.getSessionComputeGroup());
         Env.getCurrentEnv().getJobManager().triggerJob(job.getJobId(), 
mtmvTaskContext);
     }
 
-    private String getCurrentComputeGroup() {
-        if (!Config.isCloudMode()) {
-            return null;
-        }
-        ConnectContext ctx = ConnectContext.get();
-        if (ctx == null) {
-            return null;
-        }
-        try {
-            return ctx.getCloudCluster(false);
-        } catch (ComputeGroupException e) {
-            LOG.warn("failed to resolve compute group for refresh mtmv", e);
-            return null;
-        }
-    }
-
     @Override
     public void refreshComplete(MTMV mtmv, MTMVRelation relation, MTMVTask 
task) {
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPlanUtil.java 
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPlanUtil.java
index 2cbc778e735..d47ec105c4e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPlanUtil.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPlanUtil.java
@@ -36,6 +36,8 @@ import org.apache.doris.catalog.TableIf;
 import org.apache.doris.catalog.TableIf.TableType;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.cloud.qe.ComputeGroupException;
+import org.apache.doris.common.Config;
 import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.FeConstants;
@@ -153,6 +155,27 @@ public class MTMVPlanUtil {
         return ctx;
     }
 
+    /**
+     * The compute group of the current session, which a manual refresh of an 
MV that declares none
+     * borrows. Null outside cloud mode, without a session, or when the 
session resolves none.
+     */
+    @Nullable
+    public static String getSessionComputeGroup() {
+        if (!Config.isCloudMode()) {
+            return null;
+        }
+        ConnectContext ctx = ConnectContext.get();
+        if (ctx == null) {
+            return null;
+        }
+        try {
+            return ctx.getCloudCluster(false);
+        } catch (ComputeGroupException e) {
+            LOG.warn("failed to resolve compute group for refresh mtmv", e);
+            return null;
+        }
+    }
+
     /**
      * Execute a Nereids command in an MTMV context with optional audit 
logging and an
      * executor registration callback. The callback is invoked right before 
the command
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommand.java
index 3a351828293..a728038ca26 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommand.java
@@ -49,6 +49,7 @@ import org.apache.doris.qe.StmtExecutor;
 import org.apache.doris.resource.computegroup.ComputeGroupBindingUtil;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
@@ -147,25 +148,40 @@ public class RefreshMTMVCommand extends Command 
implements Forward, Explainable
     }
 
     /**
-     * The internal context the dry run plans and executes its delta query on. 
Like the refresh
-     * contexts of MTMVTask, it is bound to the compute group the MV declares, 
which is re-checked
-     * because it can have been dropped since CREATE / ALTER, so the delta 
runs, and its workload
-     * group is looked up, in the group every refresh of that MV runs in. An 
MV that declares none
-     * keeps the existing resolution.
+     * The internal context the dry run plans and executes its delta query on. 
It is bound to the
+     * compute group a manual REFRESH of the MV from this session runs in, so 
the delta runs, and its
+     * workload group is looked up, in that group: the group the MV declares, 
which is re-checked
+     * because it can have been dropped since CREATE / ALTER, or else the 
group of this session.
      */
     @VisibleForTesting
     ConnectContext createDryRunConnectContext(MTMV mtmv) throws UserException {
-        ConnectContext internalCtx = MTMVPlanUtil.createMTMVContext(
-                mtmv, MTMVPlanUtil.DISABLE_RULES_WHEN_RUN_MTMV_TASK);
         // Read once, so the group that is checked is the group that is bound.
         Optional<String> declaredComputeGroup = mtmv.getComputeGroup();
-        // A no-op outside cloud mode.
-        declaredComputeGroup.ifPresent(internalCtx::setCloudCluster);
+        ConnectContext internalCtx = createRefreshContext(mtmv, 
declaredComputeGroup);
+        // Like MTMVTask, only a declared group is re-checked: the MV never 
chose the session's group.
         
ComputeGroupBindingUtil.checkComputeGroupBeforeTask(internalCtx.getCurrentUserIdentity(),
                 declaredComputeGroup.orElse(null));
         return internalCtx;
     }
 
+    /**
+     * An internal context (admin identity, MV session variables) bound the 
way MTMVTask binds the
+     * contexts of a manual refresh: to the group the MV declares, else to the 
session's group, which
+     * MTMVJobManager#refreshMTMV hands such a refresh. Left unbound when 
there is neither, so the
+     * implicit resolution applies. The session's group is read first, while 
this session is still
+     * the thread-local context that the internal context replaces.
+     */
+    private static ConnectContext createRefreshContext(MTMV mtmv, 
Optional<String> declaredComputeGroup) {
+        String sessionComputeGroup = MTMVPlanUtil.getSessionComputeGroup();
+        ConnectContext ctx = MTMVPlanUtil.createMTMVContext(mtmv, 
MTMVPlanUtil.DISABLE_RULES_WHEN_RUN_MTMV_TASK);
+        String computeGroup = declaredComputeGroup.orElse(sessionComputeGroup);
+        if (!Strings.isNullOrEmpty(computeGroup)) {
+            // A no-op outside cloud mode.
+            ctx.setCloudCluster(computeGroup);
+        }
+        return ctx;
+    }
+
     @VisibleForTesting
     StatementContext createDryRunStatementContext(MTMV mtmv, ConnectContext 
internalCtx) {
         StatementContext stmtCtx = new StatementContext(
@@ -199,12 +215,9 @@ public class RefreshMTMVCommand extends Command implements 
Forward, Explainable
     }
 
     protected ConnectContext createExplainConnectContext(MTMV mtmv) {
-        ConnectContext explainCtx = MTMVPlanUtil.createMTMVContext(
-                mtmv, MTMVPlanUtil.DISABLE_RULES_WHEN_RUN_MTMV_TASK);
-        // Plan, and pick backends, in the group the MV declares, as MTMVTask 
does for its planning
-        // context. Nothing is executed here, so the group is not re-checked.
-        mtmv.getComputeGroup().ifPresent(explainCtx::setCloudCluster);
-        return explainCtx;
+        // Plan, and pick backends, in the group a manual REFRESH from this 
session runs in, as MTMVTask
+        // does for its planning context. Nothing is executed here, so the 
group is not re-checked.
+        return createRefreshContext(mtmv, mtmv.getComputeGroup());
     }
 
     protected LogicalPlan createRefreshCommand(MTMV mtmv, StatementContext 
statementContext) throws Exception {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommandTest.java
index e56e67909d3..eab6575a435 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RefreshMTMVCommandTest.java
@@ -60,6 +60,9 @@ import java.util.Set;
 public class RefreshMTMVCommandTest {
 
     private static final String DECLARED_CG = "cg_declared";
+    // Listed first in enterCloudMode, so it is the group admin resolves to 
when nothing is bound.
+    private static final String ADMIN_CG = "cg_of_admin";
+    private static final String SESSION_CG = "cg_of_session";
 
     private final NereidsParser parser = new NereidsParser();
 
@@ -267,6 +270,16 @@ public class RefreshMTMVCommandTest {
         Deencapsulation.setField(Env.getCurrentEnv(), "accessManager", 
accessManager);
     }
 
+    // The session that issues the statement, in this compute group as after 
`USE @<group>`; null for a
+    // session that has not chosen one.
+    private void enterSession(String computeGroup) {
+        ConnectContext session = new ConnectContext();
+        if (computeGroup != null) {
+            session.setCloudCluster(computeGroup);
+        }
+        session.setThreadLocalInfo();
+    }
+
     private MTMV newMtmvDeclaring(String declaredComputeGroup) {
         MTMV mtmv = Mockito.mock(MTMV.class);
         
Mockito.when(mtmv.getComputeGroup()).thenReturn(Optional.ofNullable(declaredComputeGroup));
@@ -315,15 +328,71 @@ public class RefreshMTMVCommandTest {
         Assertions.assertEquals(DECLARED_CG, 
explainCtx.getSessionVariable().getCloudCluster());
     }
 
-    // An MV that declares nothing keeps the existing resolution: neither 
context names a group, and
-    // the dry run has nothing to re-check, even with no compute group left at 
all.
+    // A manual REFRESH of an MV that declares no compute group runs in the 
group of the session that
+    // issues it (MTMVJobManager#refreshMTMV), so its dry run runs there too. 
Unbound, the internal
+    // context resolves a group the way admin, its identity, does: the first 
group admin may use.
+    @Test
+    public void testDryRunWithoutDeclarationRunsInSessionComputeGroup() throws 
Exception {
+        enterCloudMode(Lists.newArrayList(ADMIN_CG, SESSION_CG));
+        enterSession(SESSION_CG);
+
+        ConnectContext internalCtx = 
newIncrementalCommand(true).createDryRunConnectContext(
+                newMtmvDeclaring(null));
+
+        Assertions.assertEquals(SESSION_CG, 
internalCtx.getCloudCluster(false));
+    }
+
+    // EXPLAIN REFRESH plans, and picks backends, in the group a manual 
REFRESH from this session runs in.
+    @Test
+    public void testExplainWithoutDeclarationPlansInSessionComputeGroup() 
throws Exception {
+        enterCloudMode(Lists.newArrayList(ADMIN_CG, SESSION_CG));
+        enterSession(SESSION_CG);
+
+        ConnectContext explainCtx = 
newIncrementalCommand(false).createExplainConnectContext(
+                newMtmvDeclaring(null));
+
+        Assertions.assertEquals(SESSION_CG, explainCtx.getCloudCluster(false));
+    }
+
+    // Like a refresh (MTMVTask), the dry run re-checks only a declared group: 
the group it borrows from
+    // the session is not one the MV chose, so it is bound as it is, even 
after it is gone.
+    @Test
+    public void testDryRunRechecksOnlyTheDeclaredComputeGroup() throws 
Exception {
+        enterCloudMode(Lists.newArrayList(ADMIN_CG));
+        enterSession("cg_dropped_from_session");
+
+        ConnectContext internalCtx = 
newIncrementalCommand(true).createDryRunConnectContext(
+                newMtmvDeclaring(null));
+
+        Assertions.assertEquals("cg_dropped_from_session", 
internalCtx.getSessionVariable().getCloudCluster());
+    }
+
+    // As for a manual REFRESH, a declared group wins over the group of the 
session.
+    @Test
+    public void testDeclaredComputeGroupWinsOverSessionComputeGroup() throws 
Exception {
+        enterCloudMode(Lists.newArrayList(ADMIN_CG, SESSION_CG, DECLARED_CG));
+        MTMV mtmv = newMtmvDeclaring(DECLARED_CG);
+
+        enterSession(SESSION_CG);
+        Assertions.assertEquals(DECLARED_CG, newIncrementalCommand(true)
+                
.createDryRunConnectContext(mtmv).getSessionVariable().getCloudCluster());
+        enterSession(SESSION_CG);
+        Assertions.assertEquals(DECLARED_CG, newIncrementalCommand(false)
+                
.createExplainConnectContext(mtmv).getSessionVariable().getCloudCluster());
+    }
+
+    // With neither a declared group nor a group the session resolves, the 
existing resolution applies:
+    // neither context names a group, and the dry run has nothing to re-check, 
even with no compute
+    // group left at all.
     @Test
-    public void testContextsWithoutDeclarationKeepImplicitComputeGroup() 
throws Exception {
+    public void 
testContextsWithoutDeclarationOrSessionComputeGroupStayUnbound() throws 
Exception {
         enterCloudMode(Lists.newArrayList());
         MTMV mtmv = newMtmvDeclaring(null);
 
+        enterSession(null);
         Assertions.assertTrue(Strings.isNullOrEmpty(newIncrementalCommand(true)
                 
.createDryRunConnectContext(mtmv).getSessionVariable().getCloudCluster()));
+        enterSession(null);
         
Assertions.assertTrue(Strings.isNullOrEmpty(newIncrementalCommand(false)
                 
.createExplainConnectContext(mtmv).getSessionVariable().getCloudCluster()));
     }


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

Reply via email to