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

Jackie-Jiang 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 b72a7b8ff44 Fix flaky multi-cluster integration tests binding an 
ephemeral port (#19855)
b72a7b8ff44 is described below

commit b72a7b8ff44bad07612ef2033449eeba8b6d1b7d
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Tue Oct 13 15:56:17 2026 -0700

    Fix flaky multi-cluster integration tests binding an ephemeral port (#19855)
---
 .../BaseMultiClusterIntegrationTest.java           | 352 ++++++++++-----------
 .../multicluster/MultiClusterIntegrationTest.java  |  45 +--
 2 files changed, 197 insertions(+), 200 deletions(-)

diff --git 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/BaseMultiClusterIntegrationTest.java
 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/BaseMultiClusterIntegrationTest.java
index 24e31197d84..6537e80900a 100644
--- 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/BaseMultiClusterIntegrationTest.java
+++ 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/BaseMultiClusterIntegrationTest.java
@@ -28,6 +28,8 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.avro.Schema.Field;
+import org.apache.avro.Schema.Type;
 import org.apache.avro.file.DataFileWriter;
 import org.apache.avro.generic.GenericData;
 import org.apache.avro.generic.GenericDatumWriter;
@@ -53,6 +55,7 @@ import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.CommonConstants.Broker;
 import org.apache.pinot.spi.utils.CommonConstants.Helix;
+import org.apache.pinot.spi.utils.CommonConstants.MultiStageQueryRunner;
 import org.apache.pinot.spi.utils.CommonConstants.Server;
 import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.NetUtils;
@@ -66,9 +69,11 @@ import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+
 /// Base class for multi-cluster integration tests. Contains common 
setup/teardown logic,
 /// utility methods, and helper functions for managing multi-cluster test 
environments.
 public abstract class BaseMultiClusterIntegrationTest extends ClusterTest {
@@ -78,8 +83,6 @@ public abstract class BaseMultiClusterIntegrationTest extends 
ClusterTest {
   protected static final String TIME_COLUMN = "DaysSinceEpoch";
   protected static final String CLUSTER_1_NAME = "DualIsolatedCluster1";
   protected static final String CLUSTER_2_NAME = "DualIsolatedCluster2";
-  protected static final ClusterConfig CLUSTER_1_CONFIG = new 
ClusterConfig(CLUSTER_1_NAME, 30000);
-  protected static final ClusterConfig CLUSTER_2_CONFIG = new 
ClusterConfig(CLUSTER_2_NAME, 40000);
   protected static final String DEFAULT_TENANT = "DefaultTenant";
   protected static final int TABLE_SIZE_CLUSTER_1 = 1500;
   protected static final int TABLE_SIZE_CLUSTER_2 = 1000;
@@ -90,17 +93,18 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   protected ClusterComponents _cluster1;
   protected ClusterComponents _cluster2;
-  protected ClusterComponents _brokerWithUnavailableCluster;
+  protected BaseBrokerStarter _brokerWithUnavailableCluster;
   protected List<File> _cluster1AvroFiles;
   protected List<File> _cluster2AvroFiles;
 
   @BeforeClass
-  public void setUp() throws Exception {
+  public void setUp()
+      throws Exception {
     LOGGER.info("Setting up BaseMultiClusterIntegrationTest");
 
     // Initialize cluster components
-    _cluster1 = new ClusterComponents();
-    _cluster2 = new ClusterComponents();
+    _cluster1 = new ClusterComponents(CLUSTER_1_NAME);
+    _cluster2 = new ClusterComponents(CLUSTER_2_NAME);
 
     // Setup directories
     setupDirectories();
@@ -110,113 +114,83 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     startZookeeper(_cluster2);
 
     // Start controllers for both clusters
-    startControllerInit(_cluster1, CLUSTER_1_CONFIG);
-    startControllerInit(_cluster2, CLUSTER_2_CONFIG);
+    startController(_cluster1);
+    startController(_cluster2);
 
     // Start brokers and servers for both clusters
-    startCluster(_cluster1, _cluster2, CLUSTER_1_CONFIG);
-    startCluster(_cluster2, _cluster1, CLUSTER_2_CONFIG);
+    startCluster(_cluster1, _cluster2);
+    startCluster(_cluster2, _cluster1);
 
     // Start an alternate broker with one valid and one unavailable remote 
cluster
-    startBrokerWithUnavailableCluster();
+    _brokerWithUnavailableCluster = initBroker(_cluster1, Map.of(
+        CLUSTER_2_NAME, _cluster2._zkUrl,
+        UNAVAILABLE_CLUSTER_NAME, UNAVAILABLE_ZK_ADDRESS
+    ));
+    _brokerWithUnavailableCluster.start();
 
     LOGGER.info("BaseMultiClusterIntegrationTest setup complete");
   }
 
-  /// Starts a broker configured with cluster2 (valid) and an unavailable 
cluster (invalid ZK).
-  private void startBrokerWithUnavailableCluster() throws Exception {
-    _brokerWithUnavailableCluster = new ClusterComponents();
-    _brokerWithUnavailableCluster._brokerPort = findAvailablePort(55000);
-
-    PinotConfiguration brokerConfig = new PinotConfiguration();
-    brokerConfig.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, 
_cluster1._zkUrl);
-    brokerConfig.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, CLUSTER_1_NAME);
-    brokerConfig.setProperty(Broker.CONFIG_OF_BROKER_HOSTNAME, 
ControllerTest.LOCAL_HOST);
-    brokerConfig.setProperty(Helix.KEY_OF_BROKER_QUERY_PORT, 
_brokerWithUnavailableCluster._brokerPort);
-    brokerConfig.setProperty(Broker.CONFIG_OF_BROKER_TIMEOUT_MS, 60 * 1000L);
-    brokerConfig.setProperty(Broker.CONFIG_OF_DELAY_SHUTDOWN_TIME_MS, 0);
-    brokerConfig.setProperty(CommonConstants.CONFIG_OF_TIMEZONE, "UTC");
-    brokerConfig.setProperty(Helix.CONFIG_OF_REMOTE_CLUSTER_NAMES,
-        CLUSTER_2_NAME + "," + UNAVAILABLE_CLUSTER_NAME);
-    
brokerConfig.setProperty(String.format(Helix.CONFIG_OF_REMOTE_ZOOKEEPER_SERVERS,
 CLUSTER_2_NAME),
-        _cluster2._zkUrl);
-    
brokerConfig.setProperty(String.format(Helix.CONFIG_OF_REMOTE_ZOOKEEPER_SERVERS,
 UNAVAILABLE_CLUSTER_NAME),
-        UNAVAILABLE_ZK_ADDRESS);
-
-    _brokerWithUnavailableCluster._brokerStarter = createBrokerStarter();
-    _brokerWithUnavailableCluster._brokerStarter.init(brokerConfig);
-    _brokerWithUnavailableCluster._brokerStarter.start();
-    LOGGER.info("Started broker with unavailable cluster on port {}", 
_brokerWithUnavailableCluster._brokerPort);
-  }
-
   @Override
   protected BaseBrokerStarter createBrokerStarter() {
     return new MultiClusterHelixBrokerStarter();
   }
 
-  protected static class ClusterConfig {
-    final String _name;
-    final int _basePort;
-
-    ClusterConfig(String name, int basePort) {
-      _name = name;
-      _basePort = basePort;
-    }
-  }
-
   protected static class ClusterComponents {
+    final String _clusterName;
     ZkStarter.ZookeeperInstance _zkInstance;
     BaseControllerStarter _controllerStarter;
     BaseBrokerStarter _brokerStarter;
     BaseServerStarter _serverStarter;
-    int _controllerPort;
-    int _brokerPort;
-    int _serverPort;
     String _zkUrl;
     String _controllerBaseApiUrl;
     File _tempDir;
     File _segmentDir;
     File _tarDir;
+
+    ClusterComponents(String clusterName) {
+      _clusterName = clusterName;
+    }
   }
 
   // ========== Cluster Setup Methods ==========
 
-  protected void setupDirectories() throws Exception {
+  protected void setupDirectories()
+      throws Exception {
     setupClusterDirectories(_cluster1, "cluster1");
     setupClusterDirectories(_cluster2, "cluster2");
   }
 
-  private void setupClusterDirectories(ClusterComponents cluster, String 
clusterPrefix) throws Exception {
+  private void setupClusterDirectories(ClusterComponents cluster, String 
clusterPrefix)
+      throws Exception {
     cluster._tempDir = new File(FileUtils.getTempDirectory(), clusterPrefix + 
"_" + getClass().getSimpleName());
     cluster._segmentDir = new File(cluster._tempDir, "segmentDir");
     cluster._tarDir = new File(cluster._tempDir, "tarDir");
     TestUtils.ensureDirectoriesExistAndEmpty(cluster._tempDir, 
cluster._segmentDir, cluster._tarDir);
   }
 
-  protected void startZookeeper(ClusterComponents cluster) throws Exception {
+  protected void startZookeeper(ClusterComponents cluster)
+      throws Exception {
     cluster._zkInstance = ZkStarter.startLocalZkServer();
     cluster._zkUrl = cluster._zkInstance.getZkUrl();
   }
 
-  protected void startControllerInit(ClusterComponents cluster, ClusterConfig 
config) throws Exception {
-    cluster._controllerPort = findAvailablePort(config._basePort);
-    startController(cluster, config);
-  }
-
-  protected void startCluster(ClusterComponents cluster, ClusterComponents 
remoteCluster,
-      ClusterConfig config) throws Exception {
-    cluster._brokerPort = findAvailablePort(cluster._controllerPort + 1000);
-    startBroker(cluster, remoteCluster, config);
-    cluster._serverPort = findAvailablePort(cluster._brokerPort + 1000);
-    startServerWithMSE(cluster, config);
+  protected void startCluster(ClusterComponents cluster, ClusterComponents 
remoteCluster)
+      throws Exception {
+    cluster._brokerStarter = initBroker(cluster, 
Map.of(remoteCluster._clusterName, remoteCluster._zkUrl));
+    cluster._brokerStarter.start();
+    startServerWithMSE(cluster);
   }
 
-  protected void startController(ClusterComponents cluster, ClusterConfig 
config) throws Exception {
+  protected void startController(ClusterComponents cluster)
+      throws Exception {
+    int controllerPort = NetUtils.findOpenPort(_nextControllerPort);
+    _nextControllerPort = controllerPort + 1;
     Map<String, Object> controllerConfig = new HashMap<>();
     controllerConfig.put(ControllerConf.ZK_STR, cluster._zkUrl);
-    controllerConfig.put(ControllerConf.HELIX_CLUSTER_NAME, config._name);
+    controllerConfig.put(ControllerConf.HELIX_CLUSTER_NAME, 
cluster._clusterName);
     controllerConfig.put(ControllerConf.CONTROLLER_HOST, 
ControllerTest.LOCAL_HOST);
-    controllerConfig.put(ControllerConf.CONTROLLER_PORT, 
cluster._controllerPort);
+    controllerConfig.put(ControllerConf.CONTROLLER_PORT, controllerPort);
     controllerConfig.put(ControllerConf.DATA_DIR, 
cluster._tempDir.getAbsolutePath());
     controllerConfig.put(ControllerConf.LOCAL_TEMP_DIR, 
cluster._tempDir.getAbsolutePath());
     controllerConfig.put(ControllerConf.DISABLE_GROOVY, false);
@@ -226,42 +200,60 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     cluster._controllerStarter = createControllerStarter();
     cluster._controllerStarter.init(new PinotConfiguration(controllerConfig));
     cluster._controllerStarter.start();
-    cluster._controllerBaseApiUrl = "http://localhost:"; + 
cluster._controllerPort;
+    cluster._controllerBaseApiUrl = "http://localhost:"; + controllerPort;
   }
 
-  protected void startBroker(ClusterComponents cluster, ClusterComponents 
remoteCluster,
-      ClusterConfig config) throws Exception {
+  /// Returns an initialized, not yet started broker in `cluster` that 
federates the given remote clusters, keyed by
+  /// cluster name to ZK address.
+  ///
+  /// Ports are taken from the per-fork range of [ControllerTest] rather than 
an OS-ephemeral or fixed high port:
+  /// a broker binds them only at the end of its startup, which takes a full 
ZK connection timeout when a remote
+  /// cluster is unreachable, and the OS can hand an ephemeral port to an 
outgoing connection in the meantime.
+  private BaseBrokerStarter initBroker(ClusterComponents cluster, Map<String, 
String> remoteZkUrls)
+      throws Exception {
+    int brokerPort = NetUtils.findOpenPort(_nextBrokerPort);
+    _nextBrokerPort = brokerPort + 1;
+    int queryRunnerPort = NetUtils.findOpenPort(_nextBrokerQueryRunnerPort);
+    _nextBrokerQueryRunnerPort = queryRunnerPort + 1;
     PinotConfiguration brokerConfig = new PinotConfiguration();
     brokerConfig.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, cluster._zkUrl);
-    String remoteClusterName = CLUSTER_1_NAME.equalsIgnoreCase(config._name) ? 
CLUSTER_2_NAME : CLUSTER_1_NAME;
-    brokerConfig.setProperty(Helix.CONFIG_OF_REMOTE_CLUSTER_NAMES, 
remoteClusterName);
-    
brokerConfig.setProperty(String.format(Helix.CONFIG_OF_REMOTE_ZOOKEEPER_SERVERS,
 remoteClusterName),
-        remoteCluster._zkUrl);
-    brokerConfig.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, config._name);
+    brokerConfig.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, 
cluster._clusterName);
+    brokerConfig.setProperty(Helix.CONFIG_OF_REMOTE_CLUSTER_NAMES, 
String.join(",", remoteZkUrls.keySet()));
+    remoteZkUrls.forEach((clusterName, zkUrl) ->
+        
brokerConfig.setProperty(String.format(Helix.CONFIG_OF_REMOTE_ZOOKEEPER_SERVERS,
 clusterName), zkUrl));
     brokerConfig.setProperty(Broker.CONFIG_OF_BROKER_HOSTNAME, 
ControllerTest.LOCAL_HOST);
-    brokerConfig.setProperty(Helix.KEY_OF_BROKER_QUERY_PORT, 
cluster._brokerPort);
+    brokerConfig.setProperty(Helix.KEY_OF_BROKER_QUERY_PORT, brokerPort);
+    brokerConfig.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_RUNNER_PORT, 
queryRunnerPort);
     brokerConfig.setProperty(Broker.CONFIG_OF_BROKER_TIMEOUT_MS, 60 * 1000L);
     brokerConfig.setProperty(Broker.CONFIG_OF_DELAY_SHUTDOWN_TIME_MS, 0);
     brokerConfig.setProperty(CommonConstants.CONFIG_OF_TIMEZONE, "UTC");
-    cluster._brokerStarter = createBrokerStarter();
-    cluster._brokerStarter.init(brokerConfig);
-    cluster._brokerStarter.start();
+
+    BaseBrokerStarter brokerStarter = createBrokerStarter();
+    brokerStarter.init(brokerConfig);
+    return brokerStarter;
   }
 
-  protected void startServerWithMSE(ClusterComponents cluster, ClusterConfig 
config) throws Exception {
+  protected void startServerWithMSE(ClusterComponents cluster)
+      throws Exception {
     PinotConfiguration serverConfig = new PinotConfiguration();
     serverConfig.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, cluster._zkUrl);
-    serverConfig.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, config._name);
+    serverConfig.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, 
cluster._clusterName);
     serverConfig.setProperty(Helix.KEY_OF_SERVER_NETTY_HOST, 
ControllerTest.LOCAL_HOST);
     serverConfig.setProperty(Server.CONFIG_OF_INSTANCE_DATA_DIR, 
cluster._tempDir + "/dataDir");
     serverConfig.setProperty(Server.CONFIG_OF_INSTANCE_SEGMENT_TAR_DIR, 
cluster._tempDir + "/segmentTar");
     serverConfig.setProperty(Server.CONFIG_OF_SEGMENT_FORMAT_VERSION, "v3");
     serverConfig.setProperty(Server.CONFIG_OF_SHUTDOWN_ENABLE_QUERY_CHECK, 
false);
-    int adminPort = findAvailablePort(cluster._serverPort);
-    int nettyPort = findAvailablePort(adminPort + 1);
+    int adminPort = NetUtils.findOpenPort(_nextServerPort);
+    int nettyPort = NetUtils.findOpenPort(adminPort + 1);
+    int grpcPort = NetUtils.findOpenPort(nettyPort + 1);
+    int queryServerPort = NetUtils.findOpenPort(grpcPort + 1);
+    int queryRunnerPort = NetUtils.findOpenPort(queryServerPort + 1);
+    _nextServerPort = queryRunnerPort + 1;
     serverConfig.setProperty(Server.CONFIG_OF_ADMIN_API_PORT, adminPort);
     serverConfig.setProperty(Helix.KEY_OF_SERVER_NETTY_PORT, nettyPort);
-    serverConfig.setProperty(Server.CONFIG_OF_GRPC_PORT, 
findAvailablePort(nettyPort + 1));
+    serverConfig.setProperty(Server.CONFIG_OF_GRPC_PORT, grpcPort);
+    serverConfig.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_SERVER_PORT, 
queryServerPort);
+    serverConfig.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_RUNNER_PORT, 
queryRunnerPort);
     
serverConfig.setProperty(Server.CONFIG_OF_ENABLE_THREAD_CPU_TIME_MEASUREMENT, 
true);
     serverConfig.setProperty(CommonConstants.CONFIG_OF_TIMEZONE, "UTC");
     serverConfig.setProperty(Helix.CONFIG_OF_MULTI_STAGE_ENGINE_ENABLED, true);
@@ -271,17 +263,10 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     cluster._serverStarter.start();
   }
 
-  protected int findAvailablePort(int basePort) {
-    try {
-      return NetUtils.findOpenPort(basePort);
-    } catch (Exception e) {
-      throw new RuntimeException("Failed to find available port starting from 
" + basePort, e);
-    }
-  }
-
   // ========== Data Generation Methods ==========
 
-  protected List<File> createAvroData(int dataSize, int clusterId) throws 
Exception {
+  protected List<File> createAvroData(int dataSize, int clusterId)
+      throws Exception {
     return createAvroDataMultipleSegments(dataSize, clusterId, 1);
   }
 
@@ -301,8 +286,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
         for (int i = start; i < end; i++) {
           GenericData.Record record = new GenericData.Record(avroSchema);
           for (FieldSpec fieldSpec : schema.getAllFieldSpecs()) {
-            record.put(fieldSpec.getName(), 
generateFieldValue(fieldSpec.getName(), i, clusterId,
-                fieldSpec.getDataType()));
+            String fieldName = fieldSpec.getName();
+            record.put(fieldName, generateFieldValue(fieldName, i, clusterId, 
fieldSpec.getDataType()));
           }
           writer.append(record);
         }
@@ -314,38 +299,37 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   private org.apache.avro.Schema createAvroSchema(Schema schema) {
     org.apache.avro.Schema avroSchema = 
org.apache.avro.Schema.createRecord("myRecord", null, null, false);
-    List<org.apache.avro.Schema.Field> fields = new ArrayList<>();
+    List<Field> fields = new ArrayList<>();
 
     for (FieldSpec fieldSpec : schema.getAllFieldSpecs()) {
-      org.apache.avro.Schema.Type avroType = 
getAvroType(fieldSpec.getDataType());
-      fields.add(new org.apache.avro.Schema.Field(fieldSpec.getName(),
-          org.apache.avro.Schema.create(avroType), null, null));
+      Type avroType = getAvroType(fieldSpec.getDataType());
+      fields.add(new Field(fieldSpec.getName(), 
org.apache.avro.Schema.create(avroType), null, null));
     }
     avroSchema.setFields(fields);
     return avroSchema;
   }
 
-  private org.apache.avro.Schema.Type getAvroType(FieldSpec.DataType type) {
-    switch (type) {
-      case INT: return org.apache.avro.Schema.Type.INT;
-      case LONG: return org.apache.avro.Schema.Type.LONG;
-      case FLOAT: return org.apache.avro.Schema.Type.FLOAT;
-      case DOUBLE: return org.apache.avro.Schema.Type.DOUBLE;
-      case BOOLEAN: return org.apache.avro.Schema.Type.BOOLEAN;
-      default: return org.apache.avro.Schema.Type.STRING;
-    }
+  private Type getAvroType(FieldSpec.DataType type) {
+    return switch (type) {
+      case INT -> Type.INT;
+      case LONG -> Type.LONG;
+      case FLOAT -> Type.FLOAT;
+      case DOUBLE -> Type.DOUBLE;
+      case BOOLEAN -> Type.BOOLEAN;
+      default -> Type.STRING;
+    };
   }
 
   private Object generateFieldValue(String fieldName, int index, int 
clusterId, FieldSpec.DataType dataType) {
     int baseValue = index + (clusterId * 10000);
-    switch (dataType) {
-      case INT: return index + 10000;
-      case LONG: return (long) baseValue;
-      case FLOAT: return (float) (baseValue + 0.1);
-      case DOUBLE: return (double) (baseValue + 0.1);
-      case BOOLEAN: return (baseValue % 2) == 0;
-      default: return "cluster_" + fieldName + "_" + index;
-    }
+    return switch (dataType) {
+      case INT -> index + 10000;
+      case LONG -> (long) baseValue;
+      case FLOAT -> (float) (baseValue + 0.1);
+      case DOUBLE -> baseValue + 0.1;
+      case BOOLEAN -> (baseValue % 2) == 0;
+      default -> "cluster_" + fieldName + "_" + index;
+    };
   }
 
   // ========== Data Loading Methods ==========
@@ -359,8 +343,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
         .setTableName(tableName)
         .setTimeColumnName(TIME_COLUMN)
         .build();
-    ClusterIntegrationTestUtils.buildSegmentsFromAvro(avroFiles, tableConfig, 
schema, 0,
-        cluster._segmentDir, cluster._tarDir);
+    ClusterIntegrationTestUtils.buildSegmentsFromAvro(avroFiles, tableConfig, 
schema, 0, cluster._segmentDir,
+        cluster._tarDir);
     uploadSegmentsToCluster(tableName, cluster._tarDir, 
cluster._controllerBaseApiUrl);
     Thread.sleep(2000);
   }
@@ -375,7 +359,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     }
   }
 
-  protected void uploadSegmentsToCluster(String tableName, File tarDir, String 
controllerBaseApiUrl) throws Exception {
+  protected void uploadSegmentsToCluster(String tableName, File tarDir, String 
controllerBaseApiUrl)
+      throws Exception {
     File[] segmentTarFiles = tarDir.listFiles();
     assertNotNull(segmentTarFiles);
     assertTrue(segmentTarFiles.length > 0);
@@ -384,9 +369,9 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
     try (FileUploadDownloadClient fileUploadDownloadClient = new 
FileUploadDownloadClient()) {
       for (File segmentTarFile : segmentTarFiles) {
-        int status = 
fileUploadDownloadClient.uploadSegment(uploadSegmentHttpURI,
-                segmentTarFile.getName(), segmentTarFile, List.of(), 
tableName, TableType.OFFLINE)
-            .getStatusCode();
+        int status =
+            fileUploadDownloadClient.uploadSegment(uploadSegmentHttpURI, 
segmentTarFile.getName(), segmentTarFile,
+                List.of(), tableName, TableType.OFFLINE).getStatusCode();
         assertEquals(status, HttpStatus.SC_OK);
       }
     }
@@ -396,7 +381,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   // ========== Schema and Table Management Methods ==========
 
-  protected void createSchemaAndTableForCluster(String tableName, String 
controllerBaseApiUrl) throws IOException {
+  protected void createSchemaAndTableForCluster(String tableName, String 
controllerBaseApiUrl)
+      throws IOException {
     Schema schema = createSchema(SCHEMA_FILE);
     schema.setSchemaName(tableName);
     addSchemaToCluster(schema, controllerBaseApiUrl);
@@ -407,7 +393,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     addTableConfigToCluster(tableConfig, controllerBaseApiUrl);
   }
 
-  protected void createSchemaAndTableOnBothClusters(String tableName) throws 
Exception {
+  protected void createSchemaAndTableOnBothClusters(String tableName)
+      throws Exception {
     dropTableAndSchemaIfExists(tableName, _cluster1._controllerBaseApiUrl);
     dropTableAndSchemaIfExists(tableName, _cluster2._controllerBaseApiUrl);
     createSchemaAndTableForCluster(tableName, _cluster1._controllerBaseApiUrl);
@@ -427,14 +414,16 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     }
   }
 
-  protected void addSchemaToCluster(Schema schema, String 
controllerBaseApiUrl) throws IOException {
+  protected void addSchemaToCluster(Schema schema, String controllerBaseApiUrl)
+      throws IOException {
     String url = controllerBaseApiUrl + "/schemas";
     String schemaJson = schema.toPrettyJsonString();
     String response = ControllerTest.sendPostRequest(url, schemaJson);
     assertNotNull(response);
   }
 
-  protected void addTableConfigToCluster(TableConfig tableConfig, String 
controllerBaseApiUrl) throws IOException {
+  protected void addTableConfigToCluster(TableConfig tableConfig, String 
controllerBaseApiUrl)
+      throws IOException {
     String url = controllerBaseApiUrl + "/tables";
     String tableConfigJson = JsonUtils.objectToPrettyString(tableConfig);
     String response = ControllerTest.sendPostRequest(url, tableConfigJson);
@@ -443,9 +432,10 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   // ========== Logical Table Management Methods ==========
 
-  protected void createLogicalTable(String schemaFile,
-      Map<String, PhysicalTableConfig> physicalTableConfigMap, String 
brokerTenant, String controllerBaseApiUrl,
-      String logicalTable, String refOfflineTable, String refRealtimeTable) 
throws IOException {
+  protected void createLogicalTable(String schemaFile, Map<String, 
PhysicalTableConfig> physicalTableConfigMap,
+      String brokerTenant, String controllerBaseApiUrl, String logicalTable, 
String refOfflineTable,
+      String refRealtimeTable)
+      throws IOException {
     ControllerRequestURLBuilder urlBuilder = 
ControllerRequestURLBuilder.baseUrl(controllerBaseApiUrl);
     Schema schema = createSchema(schemaFile);
     schema.setSchemaName(logicalTable);
@@ -457,10 +447,10 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
         .setRefRealtimeTableName(refRealtimeTable)
         .setPhysicalTableConfigMap(physicalTableConfigMap)
         .build();
-    String response = 
ControllerTest.sendPostRequest(urlBuilder.forLogicalTableCreate(),
-        config.toSingleLineJsonString(), Map.of());
-    assertEquals(response, "{\"unrecognizedProperties\":{},\"status\":\"" + 
logicalTable
-        + " logical table successfully added.\"}");
+    String response =
+        ControllerTest.sendPostRequest(urlBuilder.forLogicalTableCreate(), 
config.toSingleLineJsonString(), Map.of());
+    assertEquals(response,
+        "{\"unrecognizedProperties\":{},\"status\":\"" + logicalTable + " 
logical table successfully added.\"}");
   }
 
   protected void dropLogicalTableIfExists(String logicalTableName, String 
controllerBaseApiUrl) {
@@ -469,11 +459,13 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   // ========== Query Execution Methods ==========
 
-  protected String executeQuery(String query, ClusterComponents cluster) 
throws Exception {
-    return executeQueryOnBrokerPort(query, cluster._brokerPort);
+  protected String executeQuery(String query, ClusterComponents cluster)
+      throws Exception {
+    return executeQueryOnBrokerPort(query, cluster._brokerStarter.getPort());
   }
 
-  protected String executeQueryOnBrokerPort(String query, int brokerPort) 
throws Exception {
+  protected String executeQueryOnBrokerPort(String query, int brokerPort)
+      throws Exception {
     Map<String, Object> payload = Map.of("sql", query);
     String url = "http://localhost:"; + brokerPort + "/query/sql";
     return ControllerTest.sendPostRequest(url, 
JsonUtils.objectToPrettyString(payload));
@@ -481,8 +473,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   protected long getCount(String tableName, ClusterComponents cluster, boolean 
enableMultiClusterRouting)
       throws Exception {
-    String query = "SET enableMultiClusterRouting=" + 
enableMultiClusterRouting + "; SELECT COUNT(*) as count FROM "
-        + tableName;
+    String query =
+        "SET enableMultiClusterRouting=" + enableMultiClusterRouting + "; 
SELECT COUNT(*) as count FROM " + tableName;
     return parseCountResult(executeQuery(query, cluster));
   }
 
@@ -491,7 +483,7 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
   /// Helper to verify physical table validation error when 
enableMultiClusterRouting=true is used.
   protected void assertPhysicalTableValidationError(JsonNode exceptions) {
     assertNotNull(exceptions, "Expected validation error for physical table 
with enableMultiClusterRouting=true");
-    assertTrue(exceptions.size() > 0, "Expected validation error exceptions");
+    assertFalse(exceptions.isEmpty(), "Expected validation error exceptions");
 
     boolean foundValidationError = false;
     for (JsonNode ex : exceptions) {
@@ -509,7 +501,7 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
 
   /// Helper to verify no federation-related exceptions are present.
   protected void assertNoFederationExceptions(JsonNode exceptions) {
-    if (exceptions != null && exceptions.size() > 0) {
+    if (exceptions != null && !exceptions.isEmpty()) {
       for (JsonNode ex : exceptions) {
         String message = ex.get("message").asText();
         assertTrue(!message.toLowerCase().contains("remote") && 
!message.toLowerCase().contains("federation"),
@@ -518,7 +510,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     }
   }
 
-  protected void verifyUnavailableClusterException(String result, boolean 
expectException) throws Exception {
+  protected void verifyUnavailableClusterException(String result, boolean 
expectException)
+      throws Exception {
     if (expectException) {
       assertTrue(result.contains(UNAVAILABLE_CLUSTER_NAME),
           "Response should mention unavailable cluster: " + 
UNAVAILABLE_CLUSTER_NAME);
@@ -527,8 +520,7 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
       assertNotNull(exceptions, "Exceptions array should exist");
       boolean found = false;
       for (JsonNode ex : exceptions) {
-        if (ex.get("errorCode").asInt() == 510
-            && ex.get("message").asText().contains(UNAVAILABLE_CLUSTER_NAME)) {
+        if (ex.get("errorCode").asInt() == 510 && 
ex.get("message").asText().contains(UNAVAILABLE_CLUSTER_NAME)) {
           found = true;
           break;
         }
@@ -540,9 +532,9 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
   protected long parseCountResult(String result) {
     try {
       JsonNode rows = 
JsonMapper.builder().build().readTree(result).path("resultTable").path("rows");
-      if (rows.isArray() && rows.size() > 0) {
+      if (rows.isArray() && !rows.isEmpty()) {
         JsonNode firstRow = rows.get(0);
-        if (firstRow.isArray() && firstRow.size() > 0) {
+        if (firstRow.isArray() && !firstRow.isEmpty()) {
           return Long.parseLong(firstRow.get(0).asText());
         }
       }
@@ -552,7 +544,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     return 0;
   }
 
-  protected void assertResultRows(String resultJson) throws Exception {
+  protected void assertResultRows(String resultJson)
+      throws Exception {
     JsonNode rows = 
JsonMapper.builder().build().readTree(resultJson).get("resultTable").get("rows");
     assertNotNull(rows);
     for (JsonNode row : rows) {
@@ -566,7 +559,8 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     }
   }
 
-  protected Schema createSchema(String schemaFileName) throws IOException {
+  protected Schema createSchema(String schemaFileName)
+      throws IOException {
     InputStream schemaInputStream = 
getClass().getClassLoader().getResourceAsStream(schemaFileName);
     assertNotNull(schemaInputStream, "Schema file not found: " + 
schemaFileName);
     return Schema.fromInputStream(schemaInputStream);
@@ -603,9 +597,10 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
   /// @param expectValidationError Whether to expect a validation error
   /// @param testName Test name for logging
   protected void verifyPhysicalTableLocalOnly(String physicalTableName, String 
queryOptions, int brokerPort,
-      boolean expectValidationError, String testName) throws Exception {
-    LOGGER.info("Running {} on broker port {} - physical tables should always 
query local cluster only",
-        testName, brokerPort);
+      boolean expectValidationError, String testName)
+      throws Exception {
+    LOGGER.info("Running {} on broker port {} - physical tables should always 
query local cluster only", testName,
+        brokerPort);
 
     String countQuery = queryOptions + "SELECT COUNT(*) as count FROM " + 
physicalTableName;
     String countResult = executeQueryOnBrokerPort(countQuery, brokerPort);
@@ -617,16 +612,10 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
       assertPhysicalTableValidationError(exceptions);
       LOGGER.info("Verified {} correctly rejected physical table query with 
enableMultiClusterRouting=true", testName);
     } else {
-      long expectedLocalCount = TABLE_SIZE_CLUSTER_1;
       long actualCount = parseCountResult(countResult);
-
-      assertEquals(actualCount, expectedLocalCount,
-          "Physical table should always return local cluster count");
-
+      assertEquals(actualCount, TABLE_SIZE_CLUSTER_1, "Physical table should 
always return local cluster count");
       assertNoFederationExceptions(exceptions);
-
-      LOGGER.info("Verified {} returned only local cluster data for physical 
table (count={})",
-          testName, actualCount);
+      LOGGER.info("Verified {} returned only local cluster data for physical 
table (count={})", testName, actualCount);
     }
   }
 
@@ -636,15 +625,12 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     cleanDirectories(_cluster1._segmentDir, _cluster1._tarDir, 
_cluster2._segmentDir, _cluster2._tarDir);
   }
 
-  @AfterClass
-  public void tearDown() throws Exception {
+  @AfterClass(alwaysRun = true)
+  public void tearDown()
+      throws Exception {
     // Stop the alternate broker with unavailable cluster
-    if (_brokerWithUnavailableCluster != null && 
_brokerWithUnavailableCluster._brokerStarter != null) {
-      try {
-        _brokerWithUnavailableCluster._brokerStarter.stop();
-      } catch (Exception e) {
-        LOGGER.warn("Error stopping broker with unavailable cluster", e);
-      }
+    if (_brokerWithUnavailableCluster != null) {
+      stopQuietly("broker with unavailable cluster", 
_brokerWithUnavailableCluster::stop);
     }
     stopCluster(_cluster1);
     stopCluster(_cluster2);
@@ -654,22 +640,28 @@ public abstract class BaseMultiClusterIntegrationTest 
extends ClusterTest {
     if (cluster == null) {
       return;
     }
+    if (cluster._serverStarter != null) {
+      stopQuietly("server of " + cluster._clusterName, 
cluster._serverStarter::stop);
+    }
+    if (cluster._brokerStarter != null) {
+      stopQuietly("broker of " + cluster._clusterName, 
cluster._brokerStarter::stop);
+    }
+    if (cluster._controllerStarter != null) {
+      stopQuietly("controller of " + cluster._clusterName, 
cluster._controllerStarter::stop);
+    }
+    if (cluster._zkInstance != null) {
+      stopQuietly("ZooKeeper of " + cluster._clusterName, () -> 
ZkStarter.stopLocalZkServer(cluster._zkInstance));
+    }
+    FileUtils.deleteQuietly(cluster._tempDir);
+  }
+
+  /// Runs `stopAction` and logs instead of propagating a failure, so a 
component left half-started by a failed setup
+  /// does not prevent the remaining components from being stopped.
+  private static void stopQuietly(String component, Runnable stopAction) {
     try {
-      if (cluster._serverStarter != null) {
-        cluster._serverStarter.stop();
-      }
-      if (cluster._brokerStarter != null) {
-        cluster._brokerStarter.stop();
-      }
-      if (cluster._controllerStarter != null) {
-        cluster._controllerStarter.stop();
-      }
-      if (cluster._zkInstance != null) {
-        ZkStarter.stopLocalZkServer(cluster._zkInstance);
-      }
-      FileUtils.deleteQuietly(cluster._tempDir);
+      stopAction.run();
     } catch (Exception e) {
-      LOGGER.warn("Error stopping cluster", e);
+      LOGGER.warn("Error stopping {}", component, e);
     }
   }
 }
diff --git 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/MultiClusterIntegrationTest.java
 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/MultiClusterIntegrationTest.java
index cd620c73a05..4a9c12e2a54 100644
--- 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/MultiClusterIntegrationTest.java
+++ 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/multicluster/MultiClusterIntegrationTest.java
@@ -33,6 +33,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+
 /// Integration tests for multi-cluster routing and federation.
 /// Tests federation with different table names across clusters and various 
query modes.
 public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest {
@@ -65,7 +66,8 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
   }
 
   @Test
-  public void testMultiClusterBrokerStartsAndIsQueryable() throws Exception {
+  public void testMultiClusterBrokerStartsAndIsQueryable()
+      throws Exception {
     LOGGER.info("Testing that multi-cluster broker starts successfully and is 
queryable");
 
     // Verify both clusters' brokers are running 
(MultiClusterHelixBrokerStarter)
@@ -100,16 +102,17 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
   }
 
   @BeforeGroups("query")
-  public void setupTablesForQueryTests() throws Exception {
+  public void setupTablesForQueryTests()
+      throws Exception {
     dropLogicalTableIfExists(getLogicalTableName(), 
_cluster1._controllerBaseApiUrl);
     dropLogicalTableIfExists(getLogicalTableName(), 
_cluster2._controllerBaseApiUrl);
     dropLogicalTableIfExists(getLogicalTableName2(), 
_cluster1._controllerBaseApiUrl);
     dropLogicalTableIfExists(getLogicalTableName2(), 
_cluster2._controllerBaseApiUrl);
     setupPhysicalTables();
-    createLogicalTableOnBothClusters(getLogicalTableName(),
-        getPhysicalTable1InCluster1(), getPhysicalTable1InCluster2());
-    createLogicalTableOnBothClusters(getLogicalTableName2(),
-        getPhysicalTable2InCluster1(), getPhysicalTable2InCluster2());
+    createLogicalTableOnBothClusters(getLogicalTableName(), 
getPhysicalTable1InCluster1(),
+        getPhysicalTable1InCluster2());
+    createLogicalTableOnBothClusters(getLogicalTableName2(), 
getPhysicalTable2InCluster1(),
+        getPhysicalTable2InCluster2());
     cleanSegmentDirs();
     loadDataIntoCluster(createAvroData(TABLE_SIZE_CLUSTER_1, 1), 
getPhysicalTable1InCluster1(), _cluster1);
     loadDataIntoCluster(createAvroData(TABLE_SIZE_CLUSTER_2, 2), 
getPhysicalTable1InCluster2(), _cluster2);
@@ -149,7 +152,8 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
 
   @Test(dataProvider = "queryModesWithoutMultiClusterOption", groups = "query")
   public void testQueriesWithoutMultiClusterOptions(String testName, String 
queryOptions, boolean isJoinQuery,
-      int brokerPort) throws Exception {
+      int brokerPort)
+      throws Exception {
     LOGGER.info("Running {} on broker port {} (expecting NO federation)", 
testName, brokerPort);
 
     // When enableMultiClusterRouting is NOT set, only local cluster data 
should be returned
@@ -171,8 +175,7 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
 
       for (JsonNode row : rows) {
         int count = row.get(1).asInt();
-        assertTrue(count == 1,
-            "Expected local-only join count of 1, but got " + count + " for 
row: " + row);
+        assertEquals(count, 1, "Expected local-only join count of 1, but got " 
+ count + " for row: " + row);
       }
     }
 
@@ -187,7 +190,7 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
     // Verify no federation-related exceptions
     JsonNode resultJson = JsonMapper.builder().build().readTree(countResult);
     JsonNode exceptions = resultJson.get("exceptions");
-    if (exceptions != null && exceptions.size() > 0) {
+    if (exceptions != null && !exceptions.isEmpty()) {
       for (JsonNode ex : exceptions) {
         String message = ex.get("message").asText();
         assertTrue(!message.contains("multicluster") && 
!message.contains("federation"),
@@ -208,7 +211,7 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
 
   @DataProvider(name = "queryModesWithoutMultiClusterOption")
   public Object[][] queryModesWithoutMultiClusterOption() {
-    int normalBroker = _cluster1._brokerPort;
+    int normalBroker = _cluster1._brokerStarter.getPort();
 
     String noOpts = "";
     String onlyMseOpts = "SET useMultistageEngine=true; ";
@@ -229,7 +232,7 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
 
   @DataProvider(name = "physicalTableQueryModes")
   public Object[][] physicalTableQueryModes() {
-    int normalBroker = _cluster1._brokerPort;
+    int normalBroker = _cluster1._brokerStarter.getPort();
 
     String withMultiClusterOpts = "SET enableMultiClusterRouting=true; ";
     String withMultiClusterMseOpts = "SET enableMultiClusterRouting=true; SET 
useMultistageEngine=true; ";
@@ -248,8 +251,8 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
 
   @DataProvider(name = "queryModes")
   public Object[][] queryModes() {
-    int normalBroker = _cluster1._brokerPort;
-    int unavailableBroker = _brokerWithUnavailableCluster._brokerPort;
+    int normalBroker = _cluster1._brokerStarter.getPort();
+    int unavailableBroker = _brokerWithUnavailableCluster.getPort();
 
     String sseOpts = "SET enableMultiClusterRouting=true; ";
     String mseOpts = sseOpts + "SET useMultistageEngine=true; ";
@@ -270,7 +273,8 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
 
   /// Setup physical tables based on configuration. By default, creates 
different table names
   /// in each cluster. Subclasses can override to create same-named tables.
-  protected void setupPhysicalTables() throws Exception {
+  protected void setupPhysicalTables()
+      throws Exception {
     // Cluster 1 tables
     dropTableAndSchemaIfExists(getPhysicalTable1InCluster1(), 
_cluster1._controllerBaseApiUrl);
     dropTableAndSchemaIfExists(getPhysicalTable2InCluster1(), 
_cluster1._controllerBaseApiUrl);
@@ -285,7 +289,8 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
   }
 
   protected void createLogicalTableOnBothClusters(String logicalTableName,
-      String cluster1PhysicalTable, String cluster2PhysicalTable) throws 
IOException {
+      String cluster1PhysicalTable, String cluster2PhysicalTable)
+      throws IOException {
     // Automatically detect if same-named tables: if so, both must be marked 
as multi-cluster (true)
     // If different names: local table is false, remote table is true
     boolean areSameNamedTables = 
cluster1PhysicalTable.equals(cluster2PhysicalTable);
@@ -319,9 +324,9 @@ public class MultiClusterIntegrationTest extends 
BaseMultiClusterIntegrationTest
       );
     }
 
-    createLogicalTable(SCHEMA_FILE, cluster1PhysicalTableConfigMap, 
DEFAULT_TENANT,
-        _cluster1._controllerBaseApiUrl, logicalTableName, 
cluster1PhysicalTable + "_OFFLINE", null);
-    createLogicalTable(SCHEMA_FILE, cluster2PhysicalTableConfigMap, 
DEFAULT_TENANT,
-        _cluster2._controllerBaseApiUrl, logicalTableName, 
cluster2PhysicalTable + "_OFFLINE", null);
+    createLogicalTable(SCHEMA_FILE, cluster1PhysicalTableConfigMap, 
DEFAULT_TENANT, _cluster1._controllerBaseApiUrl,
+        logicalTableName, cluster1PhysicalTable + "_OFFLINE", null);
+    createLogicalTable(SCHEMA_FILE, cluster2PhysicalTableConfigMap, 
DEFAULT_TENANT, _cluster2._controllerBaseApiUrl,
+        logicalTableName, cluster2PhysicalTable + "_OFFLINE", null);
   }
 }


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

Reply via email to