private DcInfo initializeHelixDatacenter() throws Exception {
    // For now, the first ZK endpoint (if there are more than one endpoints) will be adopted by default for initialization.
    // Note that, Ambry currently doesn't support multiple spectators, because there should be only one source of truth.
    String zkConnectStr = dcZkInfo.getZkConnectStrs().get(0);
    HelixManager manager;
    if (dcZkInfo.getDcName().equals(clusterMapConfig.clusterMapDatacenterName)) {
      manager = Objects.requireNonNull(localManager, "localManager should have been set");
    } else {
      manager = helixFactory.getZkHelixManagerAndConnect(clusterMapConfig.clusterMapClusterName, selfInstanceName,
          InstanceType.SPECTATOR, zkConnectStr, clusterMapConfig);
    }
    HelixClusterChangeHandler clusterChangeHandler =
        helixClusterManager.new HelixClusterChangeHandler(dcName, clusterMapConfig.clusterMapClusterName,
            this::onInitializationFailure, false);
    // Create Helix RoutingTableProvider of each DC to keep track of partition(replicas) state. Here, we use CURRENT
    // STATES based RoutingTableProvider to remove dependency on Helix's pipeline and reduce notification latency.
    // To elaborate more, there are two ways to instantiate a RoutingTable. 1. EXTERNAL_VIEW based, 2. CURRENT_STATES
    // based. In the former one, helix controller generates the external view and this is read by the helix spectator to
    // create the Routing table. In the latter one, CURRENT STATES are read from helix participant to participant at the
    // helix spectator to create the Routing table. According to helix team, the former one usually takes longer time
    // since it is dependent on helix controller to generate up-to-date view but has less read traffic since we have to
    // read constructed view. The latter one takes lesser time since we don't have to wait for controller to calculate
    // the view but has more read traffic.
    logger.info("Creating routing table provider associated with Helix manager at {}", zkConnectStr);
    RoutingTableProvider routingTableProvider = new RoutingTableProvider(manager, PropertyType.EXTERNALVIEW);
    logger.info("Routing table provider is created in {}", dcName);
    routingTableProvider.addRoutingTableChangeListener(clusterChangeHandler, null);
    logger.info("Registered routing table change listeners in {}", dcName);

    // The initial instance config change notification is required to populate the static cluster
    // information, and only after that is complete do we want the live instance change notification to
    // come in. We do not need to do anything extra to ensure this, however, since Helix provides the initial
    // notification for a change from within the same thread that adds the listener, in the context of the add
    // call. Therefore, when the call to add a listener returns, the initial notification will have been
    // received and handled.
    DataNodeConfigSource dataNodeConfigSource =
        helixFactory.getDataNodeConfigSource(clusterMapConfig, zkConnectStr, dataNodeConfigSourceMetrics);
    dataNodeConfigSource.addDataNodeConfigChangeListener(clusterChangeHandler);
    logger.info("Registered data node config change listeners for Helix manager at {}", zkConnectStr);
    if (dcZkInfo.getDcName().equals(clusterMapConfig.clusterMapDatacenterName)) {
      // Local datacenter
      manager.addInstanceConfigChangeListener(clusterChangeHandler);
      logger.info("Registered instance config change listeners for Helix manager at {}", zkConnectStr);
    }
    manager.addIdealStateChangeListener(clusterChangeHandler);
    logger.info("Registered ideal state change listeners for Helix manager at {}", zkConnectStr);
    // Now register listeners to get notified on live instance change in every datacenter.
    manager.addLiveInstanceChangeListener(clusterChangeHandler);
    logger.info("Registered live instance change listeners for Helix manager at {}", zkConnectStr);

    // in case initial event occurs before adding routing table listener, here we explicitly set snapshot in
    // ClusterChangeHandler. The reason is, if listener missed initial event, snapshot inside routing table
    // provider should be already populated.
    clusterChangeHandler.setRoutingTableSnapshot(routingTableProvider.getRoutingTableSnapshot());
    // the initial routing table change should populate the instanceConfigs. If it's empty that means initial
    // change didn't come and thread should wait on the init latch to ensure routing table snapshot is non-empty
    if (clusterChangeHandler.getRoutingTableSnapshot(dcName).getInstanceConfigs().isEmpty()) {
      // Periodic refresh in routing table provider is enabled by default. In worst case, routerUpdater should
      // trigger routing table change within 5 minutes
      logger.info("Routing table snapshot in {} is currently empty. Waiting for initial notification.", dcName);
      clusterChangeHandler.waitForInitNotification();
    }

    if (!clusterMapConfig.clustermapListenCrossColo && manager != localManager) {
      manager.disconnect();
      logger.info("Stopped listening to cross colo ZK server {}", zkConnectStr);
    }

    return new HelixDcInfo(dcName, dcZkInfo, manager, clusterChangeHandler, dataNodeConfigSource, routingTableProvider);
  }
Perform initialization for a helix-managed datacenter of servers.
@return the {@link DcInfo} for the datacenter.
@throws Exception if something went wrong during startup
ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixDatacenterInitializer.java