package org.apache.helix.examples;

/*
 * 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.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.helix.HelixAdmin;
import org.apache.helix.api.rebalancer.constraint.AbstractRebalanceHardConstraint;
import org.apache.helix.api.rebalancer.constraint.AbstractRebalanceSoftConstraint;
import org.apache.helix.api.rebalancer.constraint.dataprovider.CapacityProvider;
import org.apache.helix.api.rebalancer.constraint.dataprovider.PartitionWeightProvider;
import org.apache.helix.controller.common.ResourcesStateMap;
import org.apache.helix.controller.rebalancer.constraint.PartitionWeightAwareEvennessConstraint;
import org.apache.helix.controller.rebalancer.constraint.TotalCapacityConstraint;
import org.apache.helix.controller.rebalancer.constraint.dataprovider.ZkBasedCapacityProvider;
import org.apache.helix.controller.rebalancer.constraint.dataprovider.ZkBasedPartitionWeightProvider;
import org.apache.helix.manager.zk.ZKHelixAdmin;
import org.apache.helix.model.ClusterConfig;
import org.apache.helix.model.InstanceConfig;
import org.apache.helix.model.ResourceConfig;
import org.apache.helix.util.WeightAwareRebalanceUtil;
import org.apache.helix.zookeeper.zkclient.ZkServer;


public class WeightAwareRebalanceUtilExample {
  private static String ZK_ADDRESS = "localhost:2199";
  private static String CLUSTER_NAME = "RebalanceUtilExampleCluster";
  private static HelixAdmin admin;

  final static String resourceNamePrefix = "resource";
  final static int nParticipants = 9;
  final static int nResources = 10;
  final static int nPartitions = 10;
  final static int nReplicas = 3;
  final static int defaultCapacity = 500; // total = 500*7 = 3500
  final static int resourceWeight = 10; // total = 10*10*3*10 = 3000

  final static List<String> resourceNames = new ArrayList<>();
  final static List<String> instanceNames = new ArrayList<>();
  final static List<String> partitions = new ArrayList<>(nPartitions);
  final static List<ResourceConfig> resourceConfigs = new ArrayList<>();

  final static ClusterConfig clusterConfig = new ClusterConfig(CLUSTER_NAME);
  final static List<InstanceConfig> instanceConfigs = new ArrayList<>();

  private static void printAssignmentInfo(ResourcesStateMap assignment) {
    System.out.println("The result assignment is: ");
    Map<String, Integer> instanceLoad = new HashMap<>();
    for (String resource : assignment.getResourceStatesMap().keySet()) {
      System.out.println(resource + ": " + assignment.getPartitionStateMap(resource).toString());
      for (Map<String, String> stateMap : assignment.getPartitionStateMap(resource).getStateMap().values()) {
        for (String instance : stateMap.keySet()) {
          if (!instanceLoad.containsKey(instance)) {
            instanceLoad.put(instance, 0);
          }
          instanceLoad.put(instance, instanceLoad.get(instance) + resourceWeight);
        }
      }
    }
    System.out.println("Instance load: " + instanceLoad + "\n");
  }

  private static void rebalanceUtilUsage() {
    System.out.println(String.format("Start rebalancing using WeightAwareRebalanceUtil for %d resources.", nResources));

    /**
     * Init providers with resource weight and participant capacity.
     *
     * Users should have their own logic to determine this information for their applications.
     */
    PartitionWeightProvider weightProvider = new PartitionWeightProvider() {
      @Override
      public int getPartitionWeight(String resource, String partition) {
        return resourceWeight;
      }
    };
    CapacityProvider capacityProvider = new CapacityProvider() {
      @Override
      public int getParticipantCapacity(String participant) {
        return defaultCapacity;
      }

      @Override
      public int getParticipantUsage(String participant) {
        return 0;
      }
    };

    /**
     * Init constraints with providers
     * In this example, we used 2 constraints.
     *
     * capacityConstraint will ensure the participant capacity limitation is not exceed.
     * evenConstraint will ensure the distribution is even through all eligible participant.
     */
    TotalCapacityConstraint capacityConstraint =
        new TotalCapacityConstraint(weightProvider, capacityProvider);
    PartitionWeightAwareEvennessConstraint evenConstraint =
        new PartitionWeightAwareEvennessConstraint(weightProvider, capacityProvider);

    /**
     * Call the util to calculate partition assignment
     *
     * Note that clusterConfig and instanceConfigs are predefined with minimized configuration set.
     * Users can also read this information from cluster's ZK nodes using ConfigAccessor.
     * @see org.apache.helix.ConfigAccessor#getInstanceConfig(String, String)
     * @see org.apache.helix.ConfigAccessor#getClusterConfig(String)
     */
    WeightAwareRebalanceUtil util = new WeightAwareRebalanceUtil(clusterConfig, instanceConfigs);

    /**
     * For completely new assignment, call buildIncrementalRebalanceAssignment with no existingAssignment specified.
     *
     * Note that user needs to build resrouceConfigs before using the tool.
     * If the config exists in ZK, the same object can be used directly.
     * @see org.apache.helix.ConfigAccessor#getResourceConfig(String, String)
     */
    ResourcesStateMap assignment = util.buildIncrementalRebalanceAssignment(resourceConfigs, null,
        Collections.<AbstractRebalanceHardConstraint>singletonList(capacityConstraint),
        Collections.<AbstractRebalanceSoftConstraint>singletonList(evenConstraint));
    System.out.println(String.format("Finished rebalancing using WeightAwareRebalanceUtil for %d resources.", nResources));
    printAssignmentInfo(assignment);
  }

  private static void rebalanceUtilUsageWithZkBasedDataProvider() {
    System.out.println(String.format("Start rebalancing using WeightAwareRebalanceUtil and ZK based Capacity/Weight data providers for %d resources.", nResources));

    // Init a zkserver & cluster nodes for this example
    ZkServer zkServer = ExampleHelper.startZkServer(ZK_ADDRESS);
    admin = new ZKHelixAdmin(ZK_ADDRESS);
    admin.addCluster(CLUSTER_NAME, true);

    /**
     * In order to avoid re-construct capacity / usage information every time, user can choose to use ZK based providers.
     * In this example, we assume the evaluating metrics are QPS and memory.
     * In this case, 2 sets of constraints are needed.
     *
     * Init and persistent ZkBasedDataProvider.
     * 1. Create ZK based providers and init with capacity / weight information.
     * 2. Persist providers in to HelixPropertyStore.
     * 3. Read from ZK when necessary.
     */
    // For QPS
    ZkBasedPartitionWeightProvider qpsWeightProvider =
        new ZkBasedPartitionWeightProvider(ZK_ADDRESS, CLUSTER_NAME, "QPS");
    // Note that user can specify more detailed weight info for each partition.
    qpsWeightProvider.updateWeights(Collections.EMPTY_MAP, Collections.EMPTY_MAP, resourceWeight);
    ZkBasedCapacityProvider qpsCapacityProvider =
        new ZkBasedCapacityProvider(ZK_ADDRESS, CLUSTER_NAME, "QPS");
    qpsCapacityProvider.updateCapacity(Collections.EMPTY_MAP, Collections.EMPTY_MAP, defaultCapacity);
    // For Memory
    ZkBasedPartitionWeightProvider memoryWeightProvider =
        new ZkBasedPartitionWeightProvider(ZK_ADDRESS, CLUSTER_NAME, "MEM");
    // Note that user can specify more detailed capacity and usage info for each participant.
    memoryWeightProvider.updateWeights(Collections.EMPTY_MAP, Collections.EMPTY_MAP, resourceWeight);
    ZkBasedCapacityProvider memoryCapacityProvider =
        new ZkBasedCapacityProvider(ZK_ADDRESS, CLUSTER_NAME, "MEM");
    memoryCapacityProvider.updateCapacity(Collections.EMPTY_MAP, Collections.EMPTY_MAP, defaultCapacity);

    // Persist providers
    qpsCapacityProvider.persistCapacity();
    qpsWeightProvider.persistWeights();
    memoryCapacityProvider.persistCapacity();
    memoryWeightProvider.persistWeights();

    /**
     * Init constraints with ZkBasedDataProvider
     * 1. Read providers from ZK by constructing the object with same ZK address, cluster name, and dimension name
     * 2. Specify constraints with the provider. Only use soft constraint here for simplifying.
     */
    qpsWeightProvider =
        new ZkBasedPartitionWeightProvider(ZK_ADDRESS, CLUSTER_NAME, "QPS");
    qpsCapacityProvider =
        new ZkBasedCapacityProvider(ZK_ADDRESS, CLUSTER_NAME, "QPS");
    memoryWeightProvider =
        new ZkBasedPartitionWeightProvider(ZK_ADDRESS, CLUSTER_NAME, "MEM");
    memoryCapacityProvider =
        new ZkBasedCapacityProvider(ZK_ADDRESS, CLUSTER_NAME, "MEM");

    // !WARNING! Don't put providers that are not providing same type of data
    PartitionWeightAwareEvennessConstraint qpsConstraint =
        new PartitionWeightAwareEvennessConstraint(qpsWeightProvider, qpsCapacityProvider);
    PartitionWeightAwareEvennessConstraint memoryConstraint =
        new PartitionWeightAwareEvennessConstraint(memoryWeightProvider, memoryCapacityProvider);

    List<AbstractRebalanceSoftConstraint> softConstraints = new ArrayList<>();
    softConstraints.add(qpsConstraint);
    softConstraints.add(memoryConstraint);

    /**
     * Call util to calculate partition assignment.
     * Here, use the same simple config set for example. User can always customize the configs.
     */
    WeightAwareRebalanceUtil util = new WeightAwareRebalanceUtil(clusterConfig, instanceConfigs);
    ResourcesStateMap assignment =
        util.buildIncrementalRebalanceAssignment(resourceConfigs, null, Collections.EMPTY_LIST,
            softConstraints);

    ExampleHelper.stopZkServer(zkServer);

    System.out.println(String.format("Finished rebalancing using WeightAwareRebalanceUtil and ZK based Capacity/Weight data providers for %d resources.", nResources));
    printAssignmentInfo(assignment);
  }

  private static void rebalanceWithFaultZone() {
    System.out.println(String.format("Start rebalancing using WeightAwareRebalanceUtil for %d resources in a topology aware cluster.", nResources));

    /**
     * Setup cluster config and instance configs for topology information
     * 1. enable topology aware rebalance
     * 2. setup topology layout and faultzone type
     * 3. for each instance configure domain info
     *
     * If these information is already in ZK, user can read from ZK directly.
     * @see org.apache.helix.ConfigAccessor#getClusterConfig(String)
     * @see org.apache.helix.ConfigAccessor#getInstanceConfig(String, String)
     */
    ClusterConfig clusterConfig = new ClusterConfig(CLUSTER_NAME);
    clusterConfig.setTopologyAwareEnabled(true);
    clusterConfig.setTopology("/Rack/Host");
    clusterConfig.setFaultZoneType("Rack");

    List<InstanceConfig> instanceConfigs = new ArrayList<>();
    for (int i = 0; i < instanceNames.size(); i++) {
      String instance = instanceNames.get(i);
      InstanceConfig config = new InstanceConfig(instance);
      String domainStr = String.format("Rack=%s,Host=%s", i % (nParticipants / 3), instance);
      config.setDomain(domainStr);
      instanceConfigs.add(config);
      System.out.println(String.format("Set instance %s domain to be %s.", instance, domainStr));
    }

    /**
     * Init constraints in the same way as the previous examples.
     */
    PartitionWeightProvider weightProvider = new PartitionWeightProvider() {
      @Override
      public int getPartitionWeight(String resource, String partition) {
        return resourceWeight;
      }
    };
    CapacityProvider capacityProvider = new CapacityProvider() {
      @Override
      public int getParticipantCapacity(String participant) {
        return defaultCapacity;
      }

      @Override
      public int getParticipantUsage(String participant) {
        return 0;
      }
    };
    TotalCapacityConstraint capacityConstraint =
        new TotalCapacityConstraint(weightProvider, capacityProvider);
    PartitionWeightAwareEvennessConstraint evenConstraint =
        new PartitionWeightAwareEvennessConstraint(weightProvider, capacityProvider);

    /**
     * Call the util to calculate partition assignment
     */
    WeightAwareRebalanceUtil util = new WeightAwareRebalanceUtil(clusterConfig, instanceConfigs);

    ResourcesStateMap assignment = util.buildIncrementalRebalanceAssignment(resourceConfigs, null,
        Collections.<AbstractRebalanceHardConstraint>singletonList(capacityConstraint),
        Collections.<AbstractRebalanceSoftConstraint>singletonList(evenConstraint));

    System.out.println(String.format("Finished rebalancing using WeightAwareRebalanceUtil for %d resources in a topology aware cluster with %d fault zones.", nResources, nParticipants / 3));
    printAssignmentInfo(assignment);
  }

  private static void setup() {
    for (int i = 0; i < nParticipants; i++) {
      instanceNames.add("node" + i);
    }
    for (int i = 0; i < nPartitions; i++) {
      partitions.add(Integer.toString(i));
    }

    for (int i = 0; i < nResources; i++) {
      String resourceName = resourceNamePrefix + i;
      resourceNames.add(resourceName);
      ResourceConfig.Builder resourceBuilder = new ResourceConfig.Builder(resourceName);
      resourceBuilder.setStateModelDefRef("MasterSlave");
      resourceBuilder.setNumReplica(nReplicas);
      for (String partition : partitions) {
        resourceBuilder.setPreferenceList(partition, Collections.EMPTY_LIST);
      }
      resourceConfigs.add(resourceBuilder.build());
    }

    for (String instance : instanceNames) {
      InstanceConfig config = new InstanceConfig(instance);
      instanceConfigs.add(config);
    }
  }

  public static void main(String[] args) throws Exception {
    setup();
    rebalanceUtilUsage();
    rebalanceUtilUsageWithZkBasedDataProvider();
    rebalanceWithFaultZone();
  }
}
