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

englefly 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 c8eeb86e6a4 [fix](scan) Use query-available check for scan replica 
selection (#66964)
c8eeb86e6a4 is described below

commit c8eeb86e6a47ca6ee36c4a1cb21b44b9ec22b4c5
Author: minghong <[email protected]>
AuthorDate: Fri Aug 21 14:32:40 2026 +0800

    [fix](scan) Use query-available check for scan replica selection (#66964)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    A backend that is alive but not query available (query-disabled or
    shutting down) can still enter the scan candidate set built by
    OlapScanNode.addScanRangeLocations(), which only requires isAlive() and
    isMixNode(). When a tablet ends up with a single such candidate (single
    replica table, or fixed/cooldown replica narrowing), the
    LoadBalanceScanWorkerSelector selects it as the actual scan worker
    without any availability check (the replicaLocations.size()==1 branch).
    Meanwhile the MaterializationNode snapshots the query-available backend
    set when the physical plan is translated
    (NereidsPlanner.splitFragments), i.e. earlier and with a stricter
    filter. The assigned scan worker then writes its own backend_id into
    every rowid; when the materialization operator parses the rowid and
    looks the backend_id up in its rpc_struct_map built from the earlier
    snapshot, the id is missing and the query fails with
    "MaterializationSinkOperatorX failed to find rpc_struct".
    
    This change makes both places require the same predicate
    Backend.isQueryAvailable() when selecting a scan replica host:
    - OlapScanNode.addScanRangeLocations() requires isQueryAvailable() when
    building the scan candidate set (fixed replica fallback, cooldown
    replica narrowing and the replica loop), so a query-disabled but alive
    backend no longer enters the candidates; a tablet with no
    query-available replica now fails immediately with "has no queryable
    replicas".
    - LoadBalanceScanWorkerSelector.selectScanReplicaAndMinWorkloadWorker()
    checks isQueryAvailable() in the single-replica branch and throws "No
    available workers" if the only replica is not query available; the
    multi-replica branch uses the same predicate.
---
 .../worker/LoadBalanceScanWorkerSelector.java      |  5 ++-
 .../org/apache/doris/planner/OlapScanNode.java     | 12 +++---
 ...anceScanWorkerSelectorBackendSelectionTest.java | 26 +++++++++++-
 .../OlapScanNodeBackendSelectionConfigTest.java    | 48 ++++++++++++++++++++++
 4 files changed, 83 insertions(+), 8 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelector.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelector.java
index 2502879011f..f2d53a94375 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelector.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelector.java
@@ -263,6 +263,9 @@ public class LoadBalanceScanWorkerSelector implements 
ScanWorkerSelector {
         if (replicaLocations.size() == 1) {
             TScanRangeLocation replicaLocation = replicaLocations.get(0);
             DistributedPlanWorker worker = workerManager.getWorker(catalogId, 
replicaLocation.getBackendId());
+            if (!((BackendWorker) worker).getBackend().isQueryAvailable()) {
+                throw new AnalysisException("No available workers");
+            }
             ScanRanges scanRanges = new ScanRanges();
             TScanRangeParams scanReplicaParams =
                     ScanWorkerSelector.buildScanReplicaParams(tabletLocation, 
replicaLocation);
@@ -302,7 +305,7 @@ public class LoadBalanceScanWorkerSelector implements 
ScanWorkerSelector {
 
         for (TScanRangeLocation replicaLocation : replicaLocations) {
             DistributedPlanWorker worker = workerManager.getWorker(catalogId, 
replicaLocation.getBackendId());
-            if (!worker.available()) {
+            if (!((BackendWorker) worker).getBackend().isQueryAvailable()) {
                 continue;
             }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java 
b/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
index ee7331cc3df..b7fb3388e87 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
@@ -654,9 +654,9 @@ public class OlapScanNode extends ScanNode {
                     }
                     Backend backend = allBackends.get(beId);
                     // If the fixed replica is bad, then not clear the 
replicas using random replica
-                    if (backend == null || !backend.isAlive()) {
+                    if (backend == null || !backend.isQueryAvailable()) {
                         if (LOG.isDebugEnabled()) {
-                            LOG.debug("backend {} not exists or is not alive 
for replica {}", beId,
+                            LOG.debug("backend {} not exists or is not query 
available for replica {}", beId,
                                     replica.getId());
                         }
                         Collections.shuffle(replicas);
@@ -685,7 +685,7 @@ public class OlapScanNode extends ScanNode {
                     if (replicaOptional.isPresent()) {
                         Replica replica = replicaOptional.get();
                         Backend backend = 
allBackends.get(replica.getBackendIdWithoutException());
-                        if (backend != null && backend.isAlive()) {
+                        if (backend != null && backend.isQueryAvailable()) {
                             replicas.clear();
                             replicas.add(replica);
                         }
@@ -720,14 +720,14 @@ public class OlapScanNode extends ScanNode {
                     clusterException = true;
                     continue;
                 }
-                if (backend == null || !backend.isAlive()) {
+                if (backend == null || !backend.isQueryAvailable()) {
                     if (LOG.isDebugEnabled()) {
-                        LOG.debug("backend {} not exists or is not alive for 
replica {}", backendId,
+                        LOG.debug("backend {} not exists or is not query 
available for replica {}", backendId,
                                 replica.getId());
                     }
                     String err = "replica " + replica.getId() + "'s backend " 
+ backendId
                             + (backend != null ? " with tag " + 
backend.getLocationTag() : "")
-                            + " does not exist or not alive";
+                            + " does not exist or is not query available";
                     errs.add(err);
                     continue;
                 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelectorBackendSelectionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelectorBackendSelectionTest.java
index 68c99e8ccfd..5629b9da18e 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelectorBackendSelectionTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/worker/LoadBalanceScanWorkerSelectorBackendSelectionTest.java
@@ -22,6 +22,7 @@ import org.apache.doris.common.Config;
 import org.apache.doris.common.NereidsException;
 import org.apache.doris.common.UserException;
 import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.resource.BackendSelection;
 import org.apache.doris.resource.BackendSelectionManager;
@@ -178,13 +179,36 @@ class LoadBalanceScanWorkerSelectorBackendSelectionTest {
     }
 
     @Test
-    void testSingleReplicaSelectionPreservesUnavailableWorkerBehavior() throws 
Exception {
+    void testSingleReplicaSelectionRejectsUnavailableWorker() {
         Backend unavailable = backend(1L, "preferred");
         unavailable.setAlive(false);
         DistributedPlanWorkerManager workerManager = 
workerManager(unavailable);
         LoadBalanceScanWorkerSelector selector = new 
LoadBalanceScanWorkerSelector(
                 workerManager, new ConnectContext(), false);
 
+        Assertions.assertThrows(AnalysisException.class,
+                () -> select(selector, locations(location(1L)), 100L));
+    }
+
+    @Test
+    void testSingleReplicaSelectionRejectsQueryDisabledWorker() {
+        Backend queryDisabled = backend(1L, "preferred");
+        queryDisabled.setQueryDisabled(true);
+        DistributedPlanWorkerManager workerManager = 
workerManager(queryDisabled);
+        LoadBalanceScanWorkerSelector selector = new 
LoadBalanceScanWorkerSelector(
+                workerManager, new ConnectContext(), false);
+
+        Assertions.assertThrows(AnalysisException.class,
+                () -> select(selector, locations(location(1L)), 100L));
+    }
+
+    @Test
+    void testSingleReplicaSelectionKeepsAvailableWorker() throws Exception {
+        Backend available = backend(1L, "preferred");
+        DistributedPlanWorkerManager workerManager = workerManager(available);
+        LoadBalanceScanWorkerSelector selector = new 
LoadBalanceScanWorkerSelector(
+                workerManager, new ConnectContext(), false);
+
         WorkerScanRanges selected = select(selector, locations(location(1L)), 
100L);
 
         Assertions.assertEquals(1L, selected.worker.id());
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/planner/OlapScanNodeBackendSelectionConfigTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/planner/OlapScanNodeBackendSelectionConfigTest.java
index 62c8996e0ce..df0d2a0de2c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/planner/OlapScanNodeBackendSelectionConfigTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/planner/OlapScanNodeBackendSelectionConfigTest.java
@@ -288,6 +288,54 @@ class OlapScanNodeBackendSelectionConfigTest {
         }
     }
 
+    @Test
+    void testQueryDisabledBackendExcludedFromScanCandidates() throws Exception 
{
+        Replica replica = replica(1, 10);
+        Backend disabledBackend = backend(1, "group_a");
+        disabledBackend.setQueryDisabled(true);
+        Backend otherBackend = backend(2, "group_b");
+
+        Tablet tablet = Mockito.mock(Tablet.class);
+        Mockito.when(tablet.getId()).thenReturn(20L);
+        Mockito.when(tablet.getQueryableReplicas(
+                        Mockito.eq(10L), Mockito.anyMap(), Mockito.eq(false)))
+                .thenReturn(new ArrayList<>(ImmutableList.of(replica)));
+        Mockito.when(tablet.getCooldownReplicaId()).thenReturn(-1L);
+
+        UserException ex = Assertions.assertThrows(UserException.class,
+                () -> createScanRanges(tablet, disabledBackend, otherBackend));
+        Assertions.assertTrue(ex.getMessage().contains("has no queryable 
replicas"));
+    }
+
+    @Test
+    void testCooldownReplicaOnQueryDisabledBackendIsNotNarrowed() throws 
Exception {
+        Replica preferredReplica = replica(1, 10);
+        Replica cooldownReplica = replica(2, 10);
+        Backend preferredBackend = backend(1, "group_a");
+        Backend cooldownBackend = backend(2, "group_b");
+        cooldownBackend.setQueryDisabled(true);
+
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableCooldownReplicaAffinity = true;
+        ConnectContext context = Mockito.mock(ConnectContext.class);
+        QueryState queryState = Mockito.mock(QueryState.class);
+        Mockito.when(context.getSessionVariable()).thenReturn(sessionVariable);
+        Mockito.when(context.getState()).thenReturn(queryState);
+        Mockito.when(context.getStatementContext()).thenReturn(new 
StatementContext());
+        Mockito.when(context.getComputeGroupSafely()).thenReturn(null);
+
+        try (MockedStatic<ConnectContext> mockedContext = 
Mockito.mockStatic(ConnectContext.class)) {
+            mockedContext.when(ConnectContext::get).thenReturn(context);
+
+            List<TScanRangeLocations> scanRanges = createScanRanges(
+                    preferredReplica, cooldownReplica, preferredBackend, 
cooldownBackend);
+
+            Assertions.assertEquals(ImmutableList.of(1L),
+                    scanRanges.get(0).getLocations().stream()
+                            .map(location -> 
location.getBackendId()).collect(Collectors.toList()));
+        }
+    }
+
     private List<TScanRangeLocations> createScanRanges(Replica 
preferredReplica, Replica cooldownReplica,
             Backend preferredBackend, Backend cooldownBackend) throws 
Exception {
         Tablet tablet = Mockito.mock(Tablet.class);


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

Reply via email to