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

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


The following commit(s) were added to refs/heads/master by this push:
     new d085f946779 Recover unhealthy MSE workers that are not servers in the 
routing manager (#20005)
d085f946779 is described below

commit d085f946779eec3287398af31ea0effa3754aed7
Author: Yash Mayya <[email protected]>
AuthorDate: Mon Oct 26 11:28:59 2026 -0700

    Recover unhealthy MSE workers that are not servers in the routing manager 
(#20005)
---
 .../requesthandler/GrpcBrokerRequestHandler.java   |   4 +-
 .../MultiStageBrokerRequestHandler.java            |  24 ++++-
 .../SingleConnectionBrokerRequestHandler.java      |   4 +-
 .../MultiStageBrokerRequestHandlerTest.java        | 111 ++++++++++++++++++++-
 .../query/service/dispatch/QueryDispatcher.java    |  57 +++++++++--
 .../service/dispatch/QueryDispatcherTest.java      |  59 +++++++++++
 6 files changed, 240 insertions(+), 19 deletions(-)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/GrpcBrokerRequestHandler.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/GrpcBrokerRequestHandler.java
index 59a2bd4a584..dc9e85a616d 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/GrpcBrokerRequestHandler.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/GrpcBrokerRequestHandler.java
@@ -208,9 +208,11 @@ public class GrpcBrokerRequestHandler extends 
BaseSingleStageBrokerRequestHandle
   private FailureDetector.ServerState retryUnhealthyServer(String instanceId) {
     LOGGER.info("Checking gRPC connection to unhealthy server: {}", 
instanceId);
     ServerInstance serverInstance = 
_routingManager.getEnabledServerInstanceMap().get(instanceId);
+    // Not an enabled server of the routing manager, for example a disabled 
server or a server of a remote cluster.
+    // Leave it to the other retriers.
     if (serverInstance == null) {
       LOGGER.info("Failed to find enabled server: {} in routing manager, 
skipping the retry", instanceId);
-      return FailureDetector.ServerState.UNHEALTHY;
+      return FailureDetector.ServerState.UNKNOWN;
     }
 
     String hostnamePort = String.format("%s_%d", serverInstance.getHostname(), 
serverInstance.getGrpcPort());
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
index 6823d150e11..c68506ca14e 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
@@ -1262,13 +1262,27 @@ public class MultiStageBrokerRequestHandler extends 
BaseBrokerRequestHandler {
   /// Check if a server that was previously detected as unhealthy is now 
healthy.
   public FailureDetector.ServerState retryUnhealthyServer(String instanceId) {
     LOGGER.info("Checking gRPC connection to unhealthy server: {}", 
instanceId);
-    ServerInstance serverInstance = 
_routingManager.getEnabledServerInstanceMap().get(instanceId);
-    if (serverInstance == null) {
-      LOGGER.info("Failed to find enabled server: {} in routing manager, 
skipping the retry", instanceId);
-      return FailureDetector.ServerState.UNHEALTHY;
+    ServerInstance serverInstance = getEnabledServerInstance(instanceId);
+    if (serverInstance != null) {
+      return _queryDispatcher.checkConnectivityToInstance(serverInstance);
     }
+    // Queries are also dispatched to workers that no routing manager knows, 
for example workers that a custom worker
+    // manager picks. Check them at the address that a query was last 
dispatched to.
+    return _queryDispatcher.checkConnectivityToInstance(instanceId);
+  }
 
-    return _queryDispatcher.checkConnectivityToInstance(serverInstance);
+  /// Returns the enabled server with the given instance id: a server of this 
cluster, or with multi-cluster routing, a
+  /// server of a remote cluster.
+  @Nullable
+  private ServerInstance getEnabledServerInstance(String instanceId) {
+    ServerInstance serverInstance = 
_routingManager.getEnabledServerInstanceMap().get(instanceId);
+    if (serverInstance == null && _multiClusterRoutingContext != null) {
+      RoutingManager multiClusterRoutingManager = 
_multiClusterRoutingContext.getMultiClusterRoutingManager();
+      if (multiClusterRoutingManager != null) {
+        serverInstance = 
multiClusterRoutingManager.getEnabledServerInstanceMap().get(instanceId);
+      }
+    }
+    return serverInstance;
   }
 
   public static boolean isYellowError(QueryException e) {
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/SingleConnectionBrokerRequestHandler.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/SingleConnectionBrokerRequestHandler.java
index cceced717cc..d927e675ef3 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/SingleConnectionBrokerRequestHandler.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/SingleConnectionBrokerRequestHandler.java
@@ -1048,9 +1048,11 @@ public class SingleConnectionBrokerRequestHandler 
extends BaseSingleStageBrokerR
     LOGGER.info("Retrying unhealthy server: {}", instanceId);
     ServerInstance serverInstance = 
_routingManager.getEnabledServerInstanceMap().get(instanceId);
 
+    // Not an enabled server of the routing manager, for example a disabled 
server or a server of a remote cluster.
+    // Leave it to the other retriers.
     if (serverInstance == null) {
       LOGGER.info("Failed to find enabled server: {} in routing manager, 
skipping the retry", instanceId);
-      return FailureDetector.ServerState.UNHEALTHY;
+      return FailureDetector.ServerState.UNKNOWN;
     }
 
     // Could occur if the cluster is only serving multi-stage queries
diff --git 
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
 
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
index 9cd5afcee77..149228f41d5 100644
--- 
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
+++ 
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
@@ -32,11 +32,15 @@ import org.apache.pinot.broker.queryquota.QueryQuotaManager;
 import org.apache.pinot.common.config.provider.TableCache;
 import org.apache.pinot.common.datatable.StatMap;
 import org.apache.pinot.common.failuredetector.FailureDetector;
+import org.apache.pinot.common.failuredetector.FailureDetectorFactory;
 import org.apache.pinot.common.metrics.BrokerMetrics;
 import org.apache.pinot.common.response.BrokerResponse;
 import org.apache.pinot.common.response.broker.BrokerResponseNativeV2;
 import org.apache.pinot.common.response.broker.ResultTable;
+import org.apache.pinot.core.routing.MultiClusterRoutingContext;
 import org.apache.pinot.core.routing.RoutingManager;
+import org.apache.pinot.core.transport.ServerInstance;
+import 
org.apache.pinot.core.transport.server.routing.stats.ServerRoutingStatsManager;
 import org.apache.pinot.query.QueryEnvironmentTestBase;
 import org.apache.pinot.query.mailbox.MailboxService;
 import org.apache.pinot.query.planner.physical.DispatchablePlanFragment;
@@ -67,6 +71,8 @@ import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.NetUtils;
 import org.apache.pinot.sql.parsers.CalciteSqlParser;
 import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
+import org.apache.pinot.util.TestUtils;
+import org.mockito.MockedConstruction;
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -75,7 +81,11 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
@@ -383,6 +393,12 @@ public class MultiStageBrokerRequestHandlerTest extends 
QueryEnvironmentTestBase
 
   private static MultiStageBrokerRequestHandler newHandler(PinotConfiguration 
config)
       throws Exception {
+    return newHandler(config, mock(RoutingManager.class), 
mock(FailureDetector.class), null);
+  }
+
+  private static MultiStageBrokerRequestHandler newHandler(PinotConfiguration 
config, RoutingManager routingManager,
+      FailureDetector failureDetector, @Nullable MultiClusterRoutingContext 
multiClusterRoutingContext)
+      throws Exception {
     config.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_RUNNER_HOSTNAME, 
"localhost");
     config.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_RUNNER_PORT, 
Integer.toString(NetUtils.findOpenPort()));
     BrokerQueryEventListenerFactory.init(config);
@@ -394,9 +410,10 @@ public class MultiStageBrokerRequestHandlerTest extends 
QueryEnvironmentTestBase
     when(queryQuotaManager.acquireApplication(anyString())).thenReturn(true);
 
     return new MultiStageBrokerRequestHandler(config, "testBrokerId", new 
BrokerRequestIdGenerator(),
-        mock(RoutingManager.class), new AllowAllAccessControlFactory(), 
queryQuotaManager,
-        mock(TableCache.class), mock(MultiStageQueryThrottler.class), 
mock(FailureDetector.class),
-        ThreadAccountantUtils.getNoOpAccountant(), null, 
mock(WorkerManager.class), mock(WorkerManager.class)) {
+        routingManager, new AllowAllAccessControlFactory(), queryQuotaManager,
+        mock(TableCache.class), mock(MultiStageQueryThrottler.class), 
failureDetector,
+        ThreadAccountantUtils.getNoOpAccountant(), multiClusterRoutingContext, 
mock(WorkerManager.class),
+        mock(WorkerManager.class)) {
       @Override
       public void start() {
       }
@@ -408,6 +425,94 @@ public class MultiStageBrokerRequestHandlerTest extends 
QueryEnvironmentTestBase
     };
   }
 
+  /// The failure detector keeps an instance unhealthy while any retrier 
returns UNHEALTHY for it. A multi-stage worker
+  /// that is not an enabled server of the routing manager, for example a 
server of a remote cluster, must recover
+  /// once the dispatch channel to it is READY, with the retriers of all 
request handlers registered.
+  @Test
+  public void testFailureDetectorRecoversWorkerOutsideRoutingManager()
+      throws Exception {
+    String workerId = "Server_remote_8098";
+    PinotConfiguration failureDetectorConfig = new PinotConfiguration();
+    
failureDetectorConfig.setProperty(CommonConstants.Broker.FailureDetector.CONFIG_OF_TYPE,
+        CommonConstants.Broker.FailureDetector.Type.CONNECTION.name());
+    
failureDetectorConfig.setProperty(CommonConstants.Broker.FailureDetector.CONFIG_OF_RETRY_INITIAL_DELAY_MS,
 100);
+    
failureDetectorConfig.setProperty(CommonConstants.Broker.FailureDetector.CONFIG_OF_RETRY_DELAY_FACTOR,
 1);
+    // Recover only through the retriers, not because the retries ran out
+    
failureDetectorConfig.setProperty(CommonConstants.Broker.FailureDetector.CONFIG_OF_MAX_RETRIES,
 1_000);
+    FailureDetector failureDetector =
+        FailureDetectorFactory.getFailureDetector(failureDetectorConfig, 
BrokerMetrics.noop());
+    failureDetector.registerHealthyServerNotifier(instanceId -> {
+    });
+    failureDetector.registerUnhealthyServerNotifier(instanceId -> {
+    });
+    failureDetector.start();
+    RoutingManager routingManager = mock(RoutingManager.class);
+    when(routingManager.getEnabledServerInstanceMap()).thenReturn(Map.of());
+    BrokerQueryEventListenerFactory.init(new PinotConfiguration());
+    // Like the broker starter, register the retriers of the single-stage 
handlers first
+    SingleConnectionBrokerRequestHandler singleConnectionHandler =
+        new SingleConnectionBrokerRequestHandler(new PinotConfiguration(), 
"testBrokerId",
+            new BrokerRequestIdGenerator(), routingManager, new 
AllowAllAccessControlFactory(), null, null, null, null,
+            mock(ServerRoutingStatsManager.class), failureDetector, 
ThreadAccountantUtils.getNoOpAccountant(), null,
+            null);
+    GrpcBrokerRequestHandler grpcHandler =
+        new GrpcBrokerRequestHandler(new PinotConfiguration(), "testBrokerId", 
new BrokerRequestIdGenerator(),
+            routingManager, new AllowAllAccessControlFactory(), null, null, 
failureDetector,
+            ThreadAccountantUtils.getNoOpAccountant(), null);
+    AtomicReference<FailureDetector.ServerState> workerState =
+        new AtomicReference<>(FailureDetector.ServerState.UNHEALTHY);
+    try (MockedConstruction<QueryDispatcher> dispatchers = 
mockConstruction(QueryDispatcher.class,
+        (dispatcher, context) -> 
when(dispatcher.checkConnectivityToInstance(workerId)).thenAnswer(
+            invocation -> workerState.get()))) {
+      newHandler(new PinotConfiguration(), routingManager, failureDetector, 
null);
+      QueryDispatcher dispatcher = dispatchers.constructed().get(0);
+      failureDetector.markServerUnhealthy(workerId, "remote");
+
+      // The dispatch channel to the worker is not READY
+      verify(dispatcher, 
timeout(5_000L).atLeast(3)).checkConnectivityToInstance(workerId);
+      assertEquals(failureDetector.getUnhealthyServers(), Set.of(workerId));
+
+      workerState.set(FailureDetector.ServerState.HEALTHY);
+      TestUtils.waitForCondition(aVoid -> 
failureDetector.getUnhealthyServers().isEmpty(), 5_000L,
+          "The worker did not recover");
+    } finally {
+      failureDetector.stop();
+      singleConnectionHandler.shutDown();
+      grpcHandler.shutDown();
+    }
+  }
+
+  /// The retrier checks a server at its current address, in this cluster or 
with multi-cluster routing in a remote
+  /// cluster. It checks other instances at the address that a query was last 
dispatched to.
+  @Test
+  public void testRetryUnhealthyServerChecksCurrentAddress()
+      throws Exception {
+    ServerInstance localServer = mock(ServerInstance.class);
+    ServerInstance remoteServer = mock(ServerInstance.class);
+    RoutingManager routingManager = mock(RoutingManager.class);
+    
when(routingManager.getEnabledServerInstanceMap()).thenReturn(Map.of("Server_local_8098",
 localServer));
+    RoutingManager multiClusterRoutingManager = mock(RoutingManager.class);
+    when(multiClusterRoutingManager.getEnabledServerInstanceMap()).thenReturn(
+        Map.of("Server_local_8098", localServer, "Server_remote_8098", 
remoteServer));
+    MultiClusterRoutingContext multiClusterRoutingContext =
+        new MultiClusterRoutingContext(Map.of(), routingManager, 
multiClusterRoutingManager, Set.of());
+    try (MockedConstruction<QueryDispatcher> dispatchers = 
mockConstruction(QueryDispatcher.class)) {
+      MultiStageBrokerRequestHandler handler = newHandler(new 
PinotConfiguration(), routingManager,
+          mock(FailureDetector.class), multiClusterRoutingContext);
+      QueryDispatcher dispatcher = dispatchers.constructed().get(0);
+      
when(dispatcher.checkConnectivityToInstance(localServer)).thenReturn(FailureDetector.ServerState.HEALTHY);
+      
when(dispatcher.checkConnectivityToInstance(remoteServer)).thenReturn(FailureDetector.ServerState.UNHEALTHY);
+      
when(dispatcher.checkConnectivityToInstance("Server_worker_8098")).thenReturn(
+          FailureDetector.ServerState.HEALTHY);
+
+      assertEquals(handler.retryUnhealthyServer("Server_local_8098"), 
FailureDetector.ServerState.HEALTHY);
+      assertEquals(handler.retryUnhealthyServer("Server_remote_8098"), 
FailureDetector.ServerState.UNHEALTHY);
+      assertEquals(handler.retryUnhealthyServer("Server_worker_8098"), 
FailureDetector.ServerState.HEALTHY);
+      verify(dispatcher, 
never()).checkConnectivityToInstance("Server_local_8098");
+      verify(dispatcher, 
never()).checkConnectivityToInstance("Server_remote_8098");
+    }
+  }
+
   // Timeout guards against deadlock: if rewriteReduceStageForEmptyLeaves 
fails to inline
   // all MailboxReceiveNodes, the reducer will block forever polling a mailbox 
with no sender.
   @Test(timeOut = 10_000)
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
index d2897d6ef0f..318a4d3484a 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
@@ -124,8 +124,13 @@ public class QueryDispatcher implements 
PinotClusterConfigChangeListener {
   private final MailboxService _mailboxService;
   private final ExecutorService _executorService;
   private final Map<String, DispatchClient> _dispatchClientMap = new 
ConcurrentHashMap<>();
-  /// Last known dispatch address of each server, by instance id, kept by 
[#onServerEnabled] to retire the dispatch
-  /// client of an address a server has left.
+  /// Last known dispatch address of each instance, by instance id. 
[#onServerEnabled] sets it for the servers of the
+  /// routing manager, and uses it to retire the dispatch client of an address 
a server has left. Each dispatch sets it
+  /// too, so that [#checkConnectivityToInstance(String)] can check instances 
that no routing manager knows.
+  ///
+  /// Entries are never removed. There is one for each instance id that a 
query was dispatched to, or that
+  /// [#onServerEnabled] saw, so the number of instances bounds the size as 
long as instance ids are stable, as Helix
+  /// instance ids are. [#onServerEnabled] scans the values, but it runs only 
when a server is enabled.
   private final Map<String, String> _dispatchAddressByInstanceId = new 
ConcurrentHashMap<>();
   @Nullable
   private final TlsConfig _tlsConfig;
@@ -411,6 +416,7 @@ public class QueryDispatcher implements 
PinotClusterConfigChangeListener {
           expectedForServer += workerIds.size();
         }
       }
+      recordDispatchAddress(server);
       DispatchClient client = getOrCreateDispatchClient(server);
       try {
         client.submitWithStream(request, server, deadline, session, 
expectedForServer,
@@ -634,24 +640,42 @@ public class QueryDispatcher implements 
PinotClusterConfigChangeListener {
     }
   }
 
+  /// Checks the dispatch channel to a server, at its current address in a 
routing manager.
   public FailureDetector.ServerState 
checkConnectivityToInstance(ServerInstance serverInstance) {
-    String hostname = serverInstance.getHostname();
-    int port = serverInstance.getQueryServicePort();
+    return checkConnectivity(serverInstance.getInstanceId(),
+        toHostnamePortKey(serverInstance.getHostname(), 
serverInstance.getQueryServicePort()));
+  }
 
-    DispatchClient client = _dispatchClientMap.get(toHostnamePortKey(hostname, 
port));
+  /// Checks the dispatch channel to an instance, at the address where a query 
was last dispatched to it, or where
+  /// [#onServerEnabled] last found it. The failure detector uses this for 
instances that no routing manager knows, for
+  /// example workers that a custom worker manager picks. Returns 
[FailureDetector.ServerState#UNKNOWN] if there is no
+  /// dispatch channel to that address, for example because no query was 
dispatched to the instance.
+  ///
+  /// Use [#checkConnectivityToInstance(ServerInstance)] when the current 
address of the instance is known. This address
+  /// goes stale when the instance moves to another port, until the next 
dispatch or [#onServerEnabled] call for it.
+  public FailureDetector.ServerState checkConnectivityToInstance(String 
instanceId) {
+    String hostnamePortKey = _dispatchAddressByInstanceId.get(instanceId);
+    if (hostnamePortKey == null) {
+      LOGGER.debug("No dispatch address found for instance: {}", instanceId);
+      return FailureDetector.ServerState.UNKNOWN;
+    }
+    return checkConnectivity(instanceId, hostnamePortKey);
+  }
+
+  private FailureDetector.ServerState checkConnectivity(String instanceId, 
String hostnamePortKey) {
+    DispatchClient client = _dispatchClientMap.get(hostnamePortKey);
     // Could occur if the cluster is only serving single-stage queries
     if (client == null) {
-      LOGGER.debug("No DispatchClient found for server with instanceId: {}", 
serverInstance.getInstanceId());
+      LOGGER.debug("No DispatchClient found for server with instanceId: {}", 
instanceId);
       return FailureDetector.ServerState.UNKNOWN;
     }
 
     ConnectivityState connectivityState = client.getChannel().getState(true);
     if (connectivityState == ConnectivityState.READY) {
-      LOGGER.info("Successfully connected to server: {}", 
serverInstance.getInstanceId());
+      LOGGER.info("Successfully connected to server: {}", instanceId);
       return FailureDetector.ServerState.HEALTHY;
     } else {
-      LOGGER.info("Still can't connect to server: {}, current state: {}", 
serverInstance.getInstanceId(),
-          connectivityState);
+      LOGGER.info("Still can't connect to server: {}, current state: {}", 
instanceId, connectivityState);
       return FailureDetector.ServerState.UNHEALTHY;
     }
   }
@@ -694,6 +718,7 @@ public class QueryDispatcher implements 
PinotClusterConfigChangeListener {
         }
       };
       R request = requestBuilder.apply(serverInstance);
+      recordDispatchAddress(serverInstance);
       DispatchClient dispatchClient = 
getOrCreateDispatchClient(serverInstance);
 
       try {
@@ -899,6 +924,20 @@ public class QueryDispatcher implements 
PinotClusterConfigChangeListener {
             _dispatchMaxInboundMessageSizeBytes));
   }
 
+  /// Records the address of an instance that a query is dispatched to, as its 
last known address, see
+  /// [#checkConnectivityToInstance(String)]. Only dispatches record it: a 
cancel can still go to an address that the
+  /// instance has left.
+  @VisibleForTesting
+  void recordDispatchAddress(QueryServerInstance queryServerInstance) {
+    String instanceId = queryServerInstance.getInstanceId();
+    String hostnamePortKey =
+        toHostnamePortKey(queryServerInstance.getHostname(), 
queryServerInstance.getQueryServicePort());
+    // Read first, so that a dispatch to an instance that did not move does 
not write the map
+    if (!hostnamePortKey.equals(_dispatchAddressByInstanceId.get(instanceId))) 
{
+      _dispatchAddressByInstanceId.put(instanceId, hostnamePortKey);
+    }
+  }
+
   /// Called when a server becomes routable, which it does when it starts and 
when it is enabled again after it was
   /// disabled or excluded. The server may have restarted at a new address, so:
   ///
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
index ee225479c1d..384efb83279 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
@@ -209,6 +209,65 @@ public class QueryDispatcherTest extends QueryTestSet {
     }
   }
 
+  /// The failure detector checks an instance that the routing manager does 
not know, for example a server of a remote
+  /// cluster, at the address where a query was last dispatched to it.
+  @Test
+  public void testCheckConnectivityToInstanceAtLastDispatchAddress() {
+    QueryDispatcher dispatcher =
+        new QueryDispatcher(Mockito.mock(MailboxService.class), 
Mockito.mock(FailureDetector.class), null, false,
+            Duration.ofSeconds(1));
+    String instanceId = "Server_remote_8098";
+    // Nothing listens there until the server starts
+    int port = QueryTestUtils.getAvailablePort();
+    QueryServerInstance instance = new QueryServerInstance(instanceId, 
"localhost", port, 8101);
+    QueryServer queryServer = new QueryServer(port, 
Mockito.mock(QueryRunner.class));
+    try {
+      Assert.assertEquals(dispatcher.checkConnectivityToInstance(instanceId), 
FailureDetector.ServerState.UNKNOWN);
+
+      // What a dispatch does
+      dispatcher.recordDispatchAddress(instance);
+      ManagedChannel channel = 
dispatcher.getOrCreateDispatchClient(instance).getChannel();
+      Assert.assertEquals(dispatcher.checkConnectivityToInstance(instanceId), 
FailureDetector.ServerState.UNHEALTHY);
+      TestUtils.waitForCondition(aVoid -> channel.getState(false) == 
ConnectivityState.TRANSIENT_FAILURE, 10_000L,
+          "Connecting did not fail");
+      Assert.assertEquals(dispatcher.checkConnectivityToInstance(instanceId), 
FailureDetector.ServerState.UNHEALTHY);
+
+      queryServer.start();
+      TestUtils.waitForCondition(
+          aVoid -> dispatcher.checkConnectivityToInstance(instanceId) == 
FailureDetector.ServerState.HEALTHY, 10_000L,
+          "The instance did not become healthy after its server started");
+
+      // A request other than a dispatch, for example a cancel, does not 
change the address to check
+      QueryServerInstance movedInstance =
+          new QueryServerInstance(instanceId, "localhost", 
QueryTestUtils.getAvailablePort(), 8101);
+      dispatcher.getOrCreateDispatchClient(movedInstance);
+      Assert.assertEquals(dispatcher.checkConnectivityToInstance(instanceId), 
FailureDetector.ServerState.HEALTHY);
+
+      // A dispatch does: the instance moved to another port, where nothing 
listens
+      dispatcher.recordDispatchAddress(movedInstance);
+      Assert.assertEquals(dispatcher.checkConnectivityToInstance(instanceId), 
FailureDetector.ServerState.UNHEALTHY);
+    } finally {
+      dispatcher.shutdown();
+      queryServer.shutdown();
+    }
+  }
+
+  /// Each dispatch records the address of the instances it reaches, for the 
failure detector.
+  @Test
+  public void testDispatchRecordsInstanceAddress()
+      throws Exception {
+    Set<QueryServerInstance> servers = new HashSet<>();
+    DispatchableSubPlan dispatchableSubPlan = 
_queryEnvironment.planQuery("SELECT * FROM a");
+    try (QueryThreadContext ignore = QueryThreadContext.openForMseTest()) {
+      _queryDispatcher.submit(REQUEST_ID_GEN.getAndIncrement(), 
dispatchableSubPlan, 10_000L, servers, Map.of());
+    }
+    Assert.assertFalse(servers.isEmpty());
+    for (QueryServerInstance server : servers) {
+      
Assert.assertEquals(_queryDispatcher.checkConnectivityToInstance(server.getInstanceId()),
+          FailureDetector.ServerState.HEALTHY);
+    }
+  }
+
   private static ServerInstance serverInstance(String instanceId, int 
queryServerPort) {
     InstanceConfig instanceConfig = new InstanceConfig(instanceId);
     instanceConfig.setHostName("localhost");


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

Reply via email to