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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2bf1643cddc fix: Apply cloneQueryMode prior to choosing server type. 
(#20244)
2bf1643cddc is described below

commit 2bf1643cddce5e07cfe07c3687b7445413ae9b34
Author: Gian Merlino <[email protected]>
AuthorDate: Thu Sep 3 10:05:58 2026 -0700

    fix: Apply cloneQueryMode prior to choosing server type. (#20244)
    
    In "pick", ServerSelector needs to apply cloneQueryMode prior to
    choosing whether to use Historical or realtime servers. It is possible
    that all Historicals will be filtered out by cloneQueryMode, in which
    case we need to fall back to realtime.
---
 .../druid/client/selector/ServerSelector.java      | 22 +++++-
 .../druid/client/selector/ServerSelectorTest.java  | 82 ++++++++++++++++++++++
 2 files changed, 102 insertions(+), 2 deletions(-)

diff --git 
a/server/src/main/java/org/apache/druid/client/selector/ServerSelector.java 
b/server/src/main/java/org/apache/druid/client/selector/ServerSelector.java
index 54bf3b2e0fb..d47a5a42842 100644
--- a/server/src/main/java/org/apache/druid/client/selector/ServerSelector.java
+++ b/server/src/main/java/org/apache/druid/client/selector/ServerSelector.java
@@ -193,8 +193,11 @@ public class ServerSelector implements 
Overshadowable<ServerSelector>
   public <T> QueryableDruidServer pick(@Nullable Query<T> query, 
CloneQueryMode cloneQueryMode)
   {
     synchronized (this) {
-      if (!historicalServers.isEmpty()) {
-        return historicalTierStrategy.pick(query, 
filter.getQueryableServers(historicalServers, cloneQueryMode), segment.get());
+      final Int2ObjectRBTreeMap<Set<QueryableDruidServer>> 
queryableHistoricals =
+          filter.getQueryableServers(historicalServers, cloneQueryMode);
+
+      if (hasAnyServers(queryableHistoricals)) {
+        return historicalTierStrategy.pick(query, queryableHistoricals, 
segment.get());
       }
       return realtimeTierStrategy.pick(query, realtimeServers, segment.get());
     }
@@ -253,4 +256,19 @@ public class ServerSelector implements 
Overshadowable<ServerSelector>
       return (!realtimeServers.isEmpty()) && historicalServers.isEmpty();
     }
   }
+
+  /**
+   * Whether the given priority-to-servers map holds at least one server.
+   */
+  private static boolean hasAnyServers(final 
Int2ObjectRBTreeMap<Set<QueryableDruidServer>> servers)
+  {
+    for (final Set<QueryableDruidServer> priorityServers : servers.values()) {
+      if (!priorityServers.isEmpty()) {
+        return true;
+      }
+    }
+
+    // No sets, or all sets were empty.
+    return false;
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/client/selector/ServerSelectorTest.java 
b/server/src/test/java/org/apache/druid/client/selector/ServerSelectorTest.java
index 6efa1c1a3a7..01a97ed4d20 100644
--- 
a/server/src/test/java/org/apache/druid/client/selector/ServerSelectorTest.java
+++ 
b/server/src/test/java/org/apache/druid/client/selector/ServerSelectorTest.java
@@ -21,18 +21,27 @@ package org.apache.druid.client.selector;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import org.apache.druid.client.BrokerViewOfCoordinatorConfig;
 import org.apache.druid.client.DirectDruidClient;
 import org.apache.druid.client.DruidServer;
 import org.apache.druid.client.QueryableDruidServer;
+import org.apache.druid.client.coordinator.CoordinatorClient;
 import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.query.CloneQueryMode;
 import org.apache.druid.server.coordination.ServerType;
+import org.apache.druid.server.coordinator.CoordinatorDynamicConfig;
 import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.SegmentId;
 import org.apache.druid.timeline.partition.NoneShardSpec;
 import org.apache.druid.timeline.partition.TombstoneShardSpec;
 import org.easymock.EasyMock;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Map;
 
 
 public class ServerSelectorTest
@@ -171,4 +180,77 @@ public class ServerSelectorTest
     Assertions.assertTrue(selector.hasData());
   }
 
+  @Test
+  public void testPickFallsBackToRealtimeWhenEveryHistoricalIsExcluded()
+  {
+    final ServerSelector selector = makeSelector("test_clone_and_realtime");
+
+    final QueryableDruidServer cloneTarget = addServer(selector, "clone:8083", 
ServerType.HISTORICAL);
+    final QueryableDruidServer peon = addServer(selector, "peon:8100", 
ServerType.INDEXER_EXECUTOR);
+
+    Assertions.assertEquals(peon, selector.pick(null, 
CloneQueryMode.EXCLUDECLONES));
+    Assertions.assertEquals(
+        List.of(peon.getServer().getMetadata()),
+        selector.getCandidates(1, CloneQueryMode.EXCLUDECLONES)
+    );
+
+    // The clone target is queryable when clones are not excluded.
+    Assertions.assertEquals(cloneTarget, selector.pick(null, 
CloneQueryMode.INCLUDECLONES));
+  }
+
+  @Test
+  public void 
testPickReturnsNullWhenEveryHistoricalIsExcludedAndThereIsNoRealtimeServer()
+  {
+    final ServerSelector selector = makeSelector("test_clone_only");
+
+    addServer(selector, "clone:8083", ServerType.HISTORICAL);
+
+    Assertions.assertNull(selector.pick(null, CloneQueryMode.EXCLUDECLONES));
+    Assertions.assertEquals(List.of(), selector.getCandidates(1, 
CloneQueryMode.EXCLUDECLONES));
+  }
+
+  @Test
+  public void 
testPickPrefersHistoricalOverRealtimeWhenTheHistoricalIsNotExcluded()
+  {
+    final ServerSelector selector = makeSelector("test_source_and_realtime");
+
+    final QueryableDruidServer cloneSource = addServer(selector, 
"source:8083", ServerType.HISTORICAL);
+    addServer(selector, "peon:8100", ServerType.INDEXER_EXECUTOR);
+
+    Assertions.assertEquals(cloneSource, selector.pick(null, 
CloneQueryMode.EXCLUDECLONES));
+  }
+
+  /**
+   * Creates a selector whose {@link HistoricalFilter} treats "clone:8083" as 
a clone of "source:8083".
+   */
+  private static ServerSelector makeSelector(final String dataSource)
+  {
+    final BrokerViewOfCoordinatorConfig filter =
+        new 
BrokerViewOfCoordinatorConfig(Mockito.mock(CoordinatorClient.class));
+    filter.setDynamicConfig(
+        CoordinatorDynamicConfig.builder()
+                                .withCloneServers(Map.of("clone:8083", 
"source:8083"))
+                                .build()
+    );
+
+    return new ServerSelector(
+        
DataSegment.builder(SegmentId.dummy(dataSource)).shardSpec(NoneShardSpec.instance()).build(),
+        new HighestPriorityTierSelectorStrategy(new 
RandomServerSelectorStrategy()),
+        filter
+    );
+  }
+
+  private static QueryableDruidServer addServer(
+      final ServerSelector selector,
+      final String host,
+      final ServerType serverType
+  )
+  {
+    final QueryableDruidServer server = new QueryableDruidServer(
+        new DruidServer(host, host, null, 0, null, serverType, 
DruidServer.DEFAULT_TIER, 0),
+        EasyMock.createMock(DirectDruidClient.class)
+    );
+    selector.addServerAndUpdateSegment(server, selector.getSegment());
+    return server;
+  }
 }


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

Reply via email to