You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by "shauryachats (via GitHub)" <gi...@apache.org> on 2026/01/27 04:42:30 UTC

[PR] [federation] Added MULTI_CLUSTER QuickStart for multi-cluster routing [pinot]

shauryachats opened a new pull request, #17581:
URL: https://github.com/apache/pinot/pull/17581

   ## Summary
   
   This PR adds a configurable N-cluster setup which spins up N independent clusters (set N = 3 as default), creates an `orders_OFFLINE` table on each cluster, and creates a multi-cluster enabled logical table `unified_orders` on all three clusters. The logical table can be correctly queried in multi-cluster mode using `SET enableMultiClusterRouting=true;`.
   
   ## Testing
   Started the quickstart locally and verified that the multi-cluster routing is working correctly.
   
   ### QuickStart terminal output
   <img width="833" height="415" alt="Screenshot 2026-01-26 at 8 29 07 PM" src="https://github.com/user-attachments/assets/58c7809f-8db8-428a-b874-67f3a1829f90" />
   
   ### QuickStart UI
   <img width="1800" height="973" alt="Screenshot 2026-01-26 at 8 42 11 PM" src="https://github.com/user-attachments/assets/4536851a-ade6-4105-893c-2023b9a285bb" />
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org


Re: [PR] [federation] Added MULTI_CLUSTER QuickStart for multi-cluster routing [pinot]

Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #17581:
URL: https://github.com/apache/pinot/pull/17581#issuecomment-3803192680

   ## [Codecov](https://app.codecov.io/gh/apache/pinot/pull/17581?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :white_check_mark: All modified and coverable lines are covered by tests.
   :white_check_mark: Project coverage is 55.47%. Comparing base ([`234d382`](https://app.codecov.io/gh/apache/pinot/commit/234d38239643914c5c193e4f297e4d4f2432c1a7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`140e482`](https://app.codecov.io/gh/apache/pinot/commit/140e4821d7af12c66ada0b0827678d5e8d27564c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   :warning: Report is 1 commits behind head on master.
   > :exclamation:  There is a different number of reports uploaded between BASE (234d382) and HEAD (140e482). Click for more details.
   > 
   > <details><summary>HEAD has 32 uploads less than BASE</summary>
   >
   >| Flag | BASE (234d382) | HEAD (140e482) |
   >|------|------|------|
   >|java-21|5|1|
   >|unittests1|2|1|
   >|unittests|4|1|
   >|temurin|9|1|
   >|java-11|4|0|
   >|unittests2|2|0|
   >|integration|5|0|
   >|integration1|2|0|
   >|integration2|1|0|
   >|custom-integration1|2|0|
   ></details>
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   ```diff
   @@             Coverage Diff              @@
   ##             master   #17581      +/-   ##
   ============================================
   - Coverage     63.21%   55.47%   -7.75%     
   + Complexity     1476      702     -774     
   ============================================
     Files          3172     2473     -699     
     Lines        189806   140004   -49802     
     Branches      29046    22318    -6728     
   ============================================
   - Hits         119987    77665   -42322     
   + Misses        60508    55772    -4736     
   + Partials       9311     6567    -2744     
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [custom-integration1](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   | [integration](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   | [integration1](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   | [integration2](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   | [java-11](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   | [java-21](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `55.47% <ø> (-7.71%)` | :arrow_down: |
   | [temurin](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `55.47% <ø> (-7.75%)` | :arrow_down: |
   | [unittests](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `55.47% <ø> (-7.74%)` | :arrow_down: |
   | [unittests1](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `55.47% <ø> (-0.07%)` | :arrow_down: |
   | [unittests2](https://app.codecov.io/gh/apache/pinot/pull/17581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   
   Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
   </details>
   
   [:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/pinot/pull/17581?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
   :loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org


Re: [PR] [federation] Added MULTI_CLUSTER QuickStart for multi-cluster routing [pinot]

Posted by "chenboat (via GitHub)" <gi...@apache.org>.
chenboat commented on code in PR #17581:
URL: https://github.com/apache/pinot/pull/17581#discussion_r2776248748


##########
pinot-tools/src/main/java/org/apache/pinot/tools/MultiClusterQuickstart.java:
##########
@@ -0,0 +1,389 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.tools;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.hc.client5.http.entity.mime.FileBody;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.pinot.broker.broker.helix.BaseBrokerStarter;
+import org.apache.pinot.broker.broker.helix.MultiClusterHelixBrokerStarter;
+import org.apache.pinot.common.utils.ZkStarter;
+import org.apache.pinot.common.utils.http.HttpClient;
+import org.apache.pinot.controller.BaseControllerStarter;
+import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.ControllerStarter;
+import org.apache.pinot.server.starter.helix.BaseServerStarter;
+import org.apache.pinot.server.starter.helix.HelixServerStarter;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.LogicalTableConfig;
+import org.apache.pinot.spi.data.PhysicalTableConfig;
+import org.apache.pinot.spi.data.Schema;
+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.Server;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.NetUtils;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.apache.pinot.tools.admin.command.AbstractBaseAdminCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Multi-Cluster Quickstart demonstrating cross-cluster querying via logical tables.
+ * Usage: QuickStart -type MULTI_CLUSTER
+ */
+public class MultiClusterQuickstart extends QuickStartBase {
+  private static final Logger LOGGER = LoggerFactory.getLogger(MultiClusterQuickstart.class);
+  private static final int NUM_CLUSTERS = 3;
+  private static final String LOGICAL_TABLE = "unified_orders";
+  private static final String SCHEMA_PATH = "/examples/logicalTables/orders_schema.json";
+  private static final String DATA_PATH = "/examples/batch/orders/ordersUS/rawdata/ordersUS_sample.csv";
+
+  private final List<Cluster> _clusters = new ArrayList<>();
+  private File _tmpDir;
+
+  @Override
+  public List<String> types() {
+    return List.of("MULTI_CLUSTER", "MULTICLUSTER");
+  }
+
+  @Override
+  public void execute() throws Exception {
+    _tmpDir = _setCustomDataDir ? _dataDir : new File(_dataDir, "multicluster_" + System.currentTimeMillis());
+    _tmpDir.mkdirs();
+
+    printStatus(Quickstart.Color.CYAN, "***** Starting Multi-Cluster Quickstart *****");
+    try {
+      startClusters();
+      setupTables();
+      runQueries();
+      printStatus(Quickstart.Color.GREEN, "***** Multi-Cluster Quickstart Complete *****");
+      for (Cluster c : _clusters) {
+        printStatus(Quickstart.Color.GREEN, "  " + c._name + ": http://localhost:" + c._controllerPort);
+      }
+      Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
+    } catch (Exception e) {
+      LOGGER.error("Quickstart failed", e);
+      cleanup();
+      throw e;
+    }
+  }
+
+  private void startClusters() throws Exception {
+    // Initialize clusters
+    for (int i = 0; i < NUM_CLUSTERS; i++) {
+      Cluster c = new Cluster();
+      c._index = i;
+      c._name = "MultiCluster" + (i + 1);
+      c._dir = new File(_tmpDir, "cluster" + (i + 1));
+      c._dir.mkdirs();
+      c._tableName = "orders_cluster" + (i + 1);
+      _clusters.add(c);
+    }
+
+    // Start ZooKeepers
+    printStatus(Quickstart.Color.CYAN, "Starting ZooKeepers...");
+    for (Cluster c : _clusters) {
+      c._zk = ZkStarter.startLocalZkServer();
+      c._zkUrl = c._zk.getZkUrl();
+      printStatus(Quickstart.Color.GREEN, "  Started ZK for " + c._name + " at " + c._zkUrl);
+    }
+
+    // Start Controllers
+    printStatus(Quickstart.Color.CYAN, "Starting Controllers...");
+    for (Cluster c : _clusters) {
+      c._controllerPort = NetUtils.findOpenPort(9000 + c._index * 100);
+      Map<String, Object> cfg = Map.of(
+          ControllerConf.ZK_STR, c._zkUrl,
+          ControllerConf.HELIX_CLUSTER_NAME, c._name,
+          ControllerConf.CONTROLLER_HOST, "localhost",
+          ControllerConf.CONTROLLER_PORT, c._controllerPort,
+          ControllerConf.DATA_DIR, new File(c._dir, "ctrl").getAbsolutePath(),
+          ControllerConf.DISABLE_GROOVY, false,
+          CommonConstants.CONFIG_OF_TIMEZONE, "UTC");
+      c._controller = new ControllerStarter();
+      c._controller.init(new PinotConfiguration(cfg));
+      c._controller.start();
+      printStatus(Quickstart.Color.GREEN, "  Started Controller for " + c._name + " on port " + c._controllerPort);
+    }
+
+    // Start Brokers with multi-cluster config
+    printStatus(Quickstart.Color.CYAN, "Starting Brokers...");
+    for (Cluster c : _clusters) {
+      c._brokerPort = NetUtils.findOpenPort(8000 + c._index * 100);
+      PinotConfiguration cfg = new PinotConfiguration();
+      cfg.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, c._zkUrl);
+      cfg.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, c._name);
+      cfg.setProperty(Broker.CONFIG_OF_BROKER_HOSTNAME, "localhost");
+      cfg.setProperty(Helix.KEY_OF_BROKER_QUERY_PORT, c._brokerPort);
+      cfg.setProperty(Broker.CONFIG_OF_DELAY_SHUTDOWN_TIME_MS, 0);
+
+      // Configure remote clusters
+      List<String> remotes = new ArrayList<>();
+      for (Cluster r : _clusters) {
+        if (r != c) {
+          remotes.add(r._name);
+          cfg.setProperty(String.format(Helix.CONFIG_OF_REMOTE_ZOOKEEPER_SERVERS, r._name), r._zkUrl);
+        }
+      }
+      if (!remotes.isEmpty()) {
+        cfg.setProperty(Helix.CONFIG_OF_REMOTE_CLUSTER_NAMES, String.join(",", remotes));
+      }
+
+      c._broker = new MultiClusterHelixBrokerStarter();
+      c._broker.init(cfg);
+      c._broker.start();
+      printStatus(Quickstart.Color.GREEN, "  Started Broker for " + c._name + " on port " + c._brokerPort);
+    }
+
+    // Start Servers
+    printStatus(Quickstart.Color.CYAN, "Starting Servers...");
+    for (Cluster c : _clusters) {
+      int base = 7000 + c._index * 100;
+      c._serverPort = NetUtils.findOpenPort(base);
+      PinotConfiguration cfg = new PinotConfiguration();
+      cfg.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, c._zkUrl);
+      cfg.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, c._name);
+      cfg.setProperty(Helix.KEY_OF_SERVER_NETTY_HOST, "localhost");
+      cfg.setProperty(Helix.KEY_OF_SERVER_NETTY_PORT, NetUtils.findOpenPort(base + 10));
+      cfg.setProperty(Server.CONFIG_OF_ADMIN_API_PORT, c._serverPort);
+      cfg.setProperty(Server.CONFIG_OF_GRPC_PORT, NetUtils.findOpenPort(base + 20));
+      cfg.setProperty(Server.CONFIG_OF_INSTANCE_DATA_DIR, new File(c._dir, "data").getAbsolutePath());
+      cfg.setProperty(Server.CONFIG_OF_INSTANCE_SEGMENT_TAR_DIR, new File(c._dir, "seg").getAbsolutePath());
+      cfg.setProperty(Server.CONFIG_OF_SHUTDOWN_ENABLE_QUERY_CHECK, false);
+      cfg.setProperty(Helix.CONFIG_OF_MULTI_STAGE_ENGINE_ENABLED, true);
+      c._server = new HelixServerStarter();
+      c._server.init(cfg);
+      c._server.start();
+      printStatus(Quickstart.Color.GREEN, "  Started Server for " + c._name + " on port " + c._serverPort);
+    }
+
+    // Print cluster summary
+    printStatus(Quickstart.Color.GREEN, "\n***** All clusters started *****");
+    printStatus(Quickstart.Color.YELLOW, "Cluster Summary:");
+    for (Cluster c : _clusters) {
+      printStatus(Quickstart.Color.YELLOW, "  " + c._name + ":");
+      printStatus(Quickstart.Color.YELLOW, "    ZooKeeper:  http://" + c._zkUrl);
+      printStatus(Quickstart.Color.YELLOW, "    Controller: http://localhost:" + c._controllerPort);
+      printStatus(Quickstart.Color.YELLOW, "    Broker:     http://localhost:" + c._brokerPort);
+      printStatus(Quickstart.Color.YELLOW, "    Server:     http://localhost:" + c._serverPort);
+    }
+  }
+
+  private void setupTables() throws Exception {
+    printStatus(Quickstart.Color.CYAN, "***** Setting up tables *****");
+
+    // Create physical tables
+    for (Cluster c : _clusters) {
+      Schema schema = loadSchema(c._tableName);
+      uploadSchema(c, schema);
+      String tableJson = new TableConfigBuilder(TableType.OFFLINE).setTableName(c._tableName).build().toJsonString();
+      post(c.ctrlUrl("/tables"), tableJson);
+      printStatus(Quickstart.Color.GREEN, "  Physical table: " + c._tableName + " on " + c._name);
+    }
+
+    // Load data
+    for (Cluster c : _clusters) {
+      File dataFile = new File(c._dir, "data.csv");
+      generateClusterData(c, dataFile);
+      String url = c.ctrlUrl("/ingestFromFile?tableNameWithType=" + c._tableName + "_OFFLINE"
+          + "&batchConfigMapStr=" + java.net.URLEncoder.encode("{\"inputFormat\":\"csv\"}", StandardCharsets.UTF_8));
+      var entity = MultipartEntityBuilder.create()
+          .addPart("file", new FileBody(dataFile, ContentType.TEXT_PLAIN)).build();
+      new HttpClient().sendPostRequest(URI.create(url), entity, null, null);
+      printStatus(Quickstart.Color.GREEN, "  Data loaded: " + c._name);
+    }
+
+    // Create logical tables
+    for (Cluster c : _clusters) {
+      Schema schema = loadSchema(LOGICAL_TABLE);
+      uploadSchema(c, schema);
+
+      Map<String, PhysicalTableConfig> physTables = new HashMap<>();
+      for (Cluster other : _clusters) {
+        physTables.put(other._tableName + "_OFFLINE", new PhysicalTableConfig(other != c));
+      }
+      LogicalTableConfig ltc = new LogicalTableConfig();
+      ltc.setTableName(LOGICAL_TABLE);
+      ltc.setBrokerTenant("DefaultTenant");
+      ltc.setRefOfflineTableName(c._tableName + "_OFFLINE");
+      ltc.setPhysicalTableConfigMap(physTables);
+      post(c.ctrlUrl("/logicalTables"), ltc.toSingleLineJsonString());
+      printStatus(Quickstart.Color.GREEN, "  Logical table on " + c._name);
+    }
+
+    Thread.sleep(5000); // Wait for tables to be ready

Review Comment:
   Can we wait on certain conditions like table size rather than a fix amount of time?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org


Re: [PR] [federation] Added MULTI_CLUSTER QuickStart for multi-cluster routing [pinot]

Posted by "chenboat (via GitHub)" <gi...@apache.org>.
chenboat merged PR #17581:
URL: https://github.com/apache/pinot/pull/17581


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org


Re: [PR] [federation] Added MULTI_CLUSTER QuickStart for multi-cluster routing [pinot]

Posted by "shauryachats (via GitHub)" <gi...@apache.org>.
shauryachats commented on code in PR #17581:
URL: https://github.com/apache/pinot/pull/17581#discussion_r2791276341


##########
pinot-tools/src/main/java/org/apache/pinot/tools/MultiClusterQuickstart.java:
##########
@@ -0,0 +1,389 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.tools;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.hc.client5.http.entity.mime.FileBody;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.pinot.broker.broker.helix.BaseBrokerStarter;
+import org.apache.pinot.broker.broker.helix.MultiClusterHelixBrokerStarter;
+import org.apache.pinot.common.utils.ZkStarter;
+import org.apache.pinot.common.utils.http.HttpClient;
+import org.apache.pinot.controller.BaseControllerStarter;
+import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.ControllerStarter;
+import org.apache.pinot.server.starter.helix.BaseServerStarter;
+import org.apache.pinot.server.starter.helix.HelixServerStarter;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.LogicalTableConfig;
+import org.apache.pinot.spi.data.PhysicalTableConfig;
+import org.apache.pinot.spi.data.Schema;
+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.Server;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.NetUtils;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.apache.pinot.tools.admin.command.AbstractBaseAdminCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Multi-Cluster Quickstart demonstrating cross-cluster querying via logical tables.
+ * Usage: QuickStart -type MULTI_CLUSTER
+ */
+public class MultiClusterQuickstart extends QuickStartBase {
+  private static final Logger LOGGER = LoggerFactory.getLogger(MultiClusterQuickstart.class);
+  private static final int NUM_CLUSTERS = 3;
+  private static final String LOGICAL_TABLE = "unified_orders";
+  private static final String SCHEMA_PATH = "/examples/logicalTables/orders_schema.json";
+  private static final String DATA_PATH = "/examples/batch/orders/ordersUS/rawdata/ordersUS_sample.csv";
+
+  private final List<Cluster> _clusters = new ArrayList<>();
+  private File _tmpDir;
+
+  @Override
+  public List<String> types() {
+    return List.of("MULTI_CLUSTER", "MULTICLUSTER");
+  }
+
+  @Override
+  public void execute() throws Exception {
+    _tmpDir = _setCustomDataDir ? _dataDir : new File(_dataDir, "multicluster_" + System.currentTimeMillis());
+    _tmpDir.mkdirs();
+
+    printStatus(Quickstart.Color.CYAN, "***** Starting Multi-Cluster Quickstart *****");
+    try {
+      startClusters();
+      setupTables();
+      runQueries();
+      printStatus(Quickstart.Color.GREEN, "***** Multi-Cluster Quickstart Complete *****");
+      for (Cluster c : _clusters) {
+        printStatus(Quickstart.Color.GREEN, "  " + c._name + ": http://localhost:" + c._controllerPort);
+      }
+      Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
+    } catch (Exception e) {
+      LOGGER.error("Quickstart failed", e);
+      cleanup();
+      throw e;
+    }
+  }
+
+  private void startClusters() throws Exception {
+    // Initialize clusters
+    for (int i = 0; i < NUM_CLUSTERS; i++) {
+      Cluster c = new Cluster();
+      c._index = i;
+      c._name = "MultiCluster" + (i + 1);
+      c._dir = new File(_tmpDir, "cluster" + (i + 1));
+      c._dir.mkdirs();
+      c._tableName = "orders_cluster" + (i + 1);
+      _clusters.add(c);
+    }
+
+    // Start ZooKeepers
+    printStatus(Quickstart.Color.CYAN, "Starting ZooKeepers...");
+    for (Cluster c : _clusters) {
+      c._zk = ZkStarter.startLocalZkServer();
+      c._zkUrl = c._zk.getZkUrl();
+      printStatus(Quickstart.Color.GREEN, "  Started ZK for " + c._name + " at " + c._zkUrl);
+    }
+
+    // Start Controllers
+    printStatus(Quickstart.Color.CYAN, "Starting Controllers...");
+    for (Cluster c : _clusters) {
+      c._controllerPort = NetUtils.findOpenPort(9000 + c._index * 100);
+      Map<String, Object> cfg = Map.of(
+          ControllerConf.ZK_STR, c._zkUrl,
+          ControllerConf.HELIX_CLUSTER_NAME, c._name,
+          ControllerConf.CONTROLLER_HOST, "localhost",
+          ControllerConf.CONTROLLER_PORT, c._controllerPort,
+          ControllerConf.DATA_DIR, new File(c._dir, "ctrl").getAbsolutePath(),
+          ControllerConf.DISABLE_GROOVY, false,
+          CommonConstants.CONFIG_OF_TIMEZONE, "UTC");
+      c._controller = new ControllerStarter();
+      c._controller.init(new PinotConfiguration(cfg));
+      c._controller.start();
+      printStatus(Quickstart.Color.GREEN, "  Started Controller for " + c._name + " on port " + c._controllerPort);
+    }
+
+    // Start Brokers with multi-cluster config
+    printStatus(Quickstart.Color.CYAN, "Starting Brokers...");
+    for (Cluster c : _clusters) {
+      c._brokerPort = NetUtils.findOpenPort(8000 + c._index * 100);
+      PinotConfiguration cfg = new PinotConfiguration();
+      cfg.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, c._zkUrl);
+      cfg.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, c._name);
+      cfg.setProperty(Broker.CONFIG_OF_BROKER_HOSTNAME, "localhost");
+      cfg.setProperty(Helix.KEY_OF_BROKER_QUERY_PORT, c._brokerPort);
+      cfg.setProperty(Broker.CONFIG_OF_DELAY_SHUTDOWN_TIME_MS, 0);
+
+      // Configure remote clusters
+      List<String> remotes = new ArrayList<>();
+      for (Cluster r : _clusters) {
+        if (r != c) {
+          remotes.add(r._name);
+          cfg.setProperty(String.format(Helix.CONFIG_OF_REMOTE_ZOOKEEPER_SERVERS, r._name), r._zkUrl);
+        }
+      }
+      if (!remotes.isEmpty()) {
+        cfg.setProperty(Helix.CONFIG_OF_REMOTE_CLUSTER_NAMES, String.join(",", remotes));
+      }
+
+      c._broker = new MultiClusterHelixBrokerStarter();
+      c._broker.init(cfg);
+      c._broker.start();
+      printStatus(Quickstart.Color.GREEN, "  Started Broker for " + c._name + " on port " + c._brokerPort);
+    }
+
+    // Start Servers
+    printStatus(Quickstart.Color.CYAN, "Starting Servers...");
+    for (Cluster c : _clusters) {
+      int base = 7000 + c._index * 100;
+      c._serverPort = NetUtils.findOpenPort(base);
+      PinotConfiguration cfg = new PinotConfiguration();
+      cfg.setProperty(Helix.CONFIG_OF_ZOOKEEPER_SERVER, c._zkUrl);
+      cfg.setProperty(Helix.CONFIG_OF_CLUSTER_NAME, c._name);
+      cfg.setProperty(Helix.KEY_OF_SERVER_NETTY_HOST, "localhost");
+      cfg.setProperty(Helix.KEY_OF_SERVER_NETTY_PORT, NetUtils.findOpenPort(base + 10));
+      cfg.setProperty(Server.CONFIG_OF_ADMIN_API_PORT, c._serverPort);
+      cfg.setProperty(Server.CONFIG_OF_GRPC_PORT, NetUtils.findOpenPort(base + 20));
+      cfg.setProperty(Server.CONFIG_OF_INSTANCE_DATA_DIR, new File(c._dir, "data").getAbsolutePath());
+      cfg.setProperty(Server.CONFIG_OF_INSTANCE_SEGMENT_TAR_DIR, new File(c._dir, "seg").getAbsolutePath());
+      cfg.setProperty(Server.CONFIG_OF_SHUTDOWN_ENABLE_QUERY_CHECK, false);
+      cfg.setProperty(Helix.CONFIG_OF_MULTI_STAGE_ENGINE_ENABLED, true);
+      c._server = new HelixServerStarter();
+      c._server.init(cfg);
+      c._server.start();
+      printStatus(Quickstart.Color.GREEN, "  Started Server for " + c._name + " on port " + c._serverPort);
+    }
+
+    // Print cluster summary
+    printStatus(Quickstart.Color.GREEN, "\n***** All clusters started *****");
+    printStatus(Quickstart.Color.YELLOW, "Cluster Summary:");
+    for (Cluster c : _clusters) {
+      printStatus(Quickstart.Color.YELLOW, "  " + c._name + ":");
+      printStatus(Quickstart.Color.YELLOW, "    ZooKeeper:  http://" + c._zkUrl);
+      printStatus(Quickstart.Color.YELLOW, "    Controller: http://localhost:" + c._controllerPort);
+      printStatus(Quickstart.Color.YELLOW, "    Broker:     http://localhost:" + c._brokerPort);
+      printStatus(Quickstart.Color.YELLOW, "    Server:     http://localhost:" + c._serverPort);
+    }
+  }
+
+  private void setupTables() throws Exception {
+    printStatus(Quickstart.Color.CYAN, "***** Setting up tables *****");
+
+    // Create physical tables
+    for (Cluster c : _clusters) {
+      Schema schema = loadSchema(c._tableName);
+      uploadSchema(c, schema);
+      String tableJson = new TableConfigBuilder(TableType.OFFLINE).setTableName(c._tableName).build().toJsonString();
+      post(c.ctrlUrl("/tables"), tableJson);
+      printStatus(Quickstart.Color.GREEN, "  Physical table: " + c._tableName + " on " + c._name);
+    }
+
+    // Load data
+    for (Cluster c : _clusters) {
+      File dataFile = new File(c._dir, "data.csv");
+      generateClusterData(c, dataFile);
+      String url = c.ctrlUrl("/ingestFromFile?tableNameWithType=" + c._tableName + "_OFFLINE"
+          + "&batchConfigMapStr=" + java.net.URLEncoder.encode("{\"inputFormat\":\"csv\"}", StandardCharsets.UTF_8));
+      var entity = MultipartEntityBuilder.create()
+          .addPart("file", new FileBody(dataFile, ContentType.TEXT_PLAIN)).build();
+      new HttpClient().sendPostRequest(URI.create(url), entity, null, null);
+      printStatus(Quickstart.Color.GREEN, "  Data loaded: " + c._name);
+    }
+
+    // Create logical tables
+    for (Cluster c : _clusters) {
+      Schema schema = loadSchema(LOGICAL_TABLE);
+      uploadSchema(c, schema);
+
+      Map<String, PhysicalTableConfig> physTables = new HashMap<>();
+      for (Cluster other : _clusters) {
+        physTables.put(other._tableName + "_OFFLINE", new PhysicalTableConfig(other != c));
+      }
+      LogicalTableConfig ltc = new LogicalTableConfig();
+      ltc.setTableName(LOGICAL_TABLE);
+      ltc.setBrokerTenant("DefaultTenant");
+      ltc.setRefOfflineTableName(c._tableName + "_OFFLINE");
+      ltc.setPhysicalTableConfigMap(physTables);
+      post(c.ctrlUrl("/logicalTables"), ltc.toSingleLineJsonString());
+      printStatus(Quickstart.Color.GREEN, "  Logical table on " + c._name);
+    }
+
+    Thread.sleep(5000); // Wait for tables to be ready

Review Comment:
   Good call @chenboat, addressed.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org