/* * Copyright 2008-2013 LinkedIn, Inc * * Licensed 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 voldemort.cluster; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.TreeSet; import voldemort.VoldemortException; import voldemort.annotations.concurrency.Threadsafe; import voldemort.annotations.jmx.JmxGetter; import voldemort.annotations.jmx.JmxManaged; import voldemort.utils.Utils; import com.google.common.collect.Sets; /** * A representation of the voldemort cluster * * */ @Threadsafe @JmxManaged(description = "Metadata about the physical servers on which the Voldemort cluster runs") public class Cluster implements Serializable { private static final long serialVersionUID = 1; private final String name; private final int numberOfPartitionIds; private final Map<Integer, Node> nodesById; private final Map<Integer, Zone> zonesById; private final Map<Zone, List<Integer>> nodesPerZone; private final Map<Zone, List<Integer>> partitionsPerZone; // Since partitionId space must be dense, arrays could be used instead of // maps. To do so, the partition ID range would have to be determined. This // could be done by summing up the lengths of each node's .getPartitionIds() // returned list. This could be faster to construct and lookup by some // constant and memory footprint could be better. private final Map<Integer, Zone> partitionIdToZone; private final Node[] partitionIdToNodeArray; private final Map<Integer, Node> partitionIdToNode; private final Map<Integer, Integer> partitionIdToNodeId; private final List<Node> nodesShuffled; public Cluster(String name, List<Node> nodes) { this(name, nodes, new ArrayList<Zone>()); } public Cluster(String name, List<Node> nodes, List<Zone> zones) { this.name = Utils.notNull(name); this.partitionsPerZone = new LinkedHashMap<Zone, List<Integer>>(); this.nodesPerZone = new LinkedHashMap<Zone, List<Integer>>(); this.partitionIdToZone = new HashMap<Integer, Zone>(); Map<Integer, Node> partitionIdToNodeMap = new HashMap<Integer, Node>(); this.partitionIdToNode = new HashMap<Integer, Node>(); this.partitionIdToNodeId = new HashMap<Integer, Integer>(); if(zones.size() != 0) { zonesById = new LinkedHashMap<Integer, Zone>(zones.size()); for(Zone zone: zones) { if(zonesById.containsKey(zone.getId())) throw new IllegalArgumentException("Zone id " + zone.getId() + " appears twice in the zone list."); zonesById.put(zone.getId(), zone); nodesPerZone.put(zone, new ArrayList<Integer>()); partitionsPerZone.put(zone, new ArrayList<Integer>()); } } else { // Add default zone zonesById = new LinkedHashMap<Integer, Zone>(1); Zone defaultZone = new Zone(); zonesById.put(defaultZone.getId(), defaultZone); nodesPerZone.put(defaultZone, new ArrayList<Integer>()); partitionsPerZone.put(defaultZone, new ArrayList<Integer>()); } this.nodesById = new LinkedHashMap<Integer, Node>(nodes.size()); for(Node node: nodes) { if(nodesById.containsKey(node.getId())) throw new IllegalArgumentException("Node id " + node.getId() + " appears twice in the node list."); nodesById.put(node.getId(), node); Zone nodesZone = zonesById.get(node.getZoneId()); if(nodesZone == null) { throw new IllegalArgumentException("No zone associated with this node exists."); } nodesPerZone.get(nodesZone).add(node.getId()); partitionsPerZone.get(nodesZone).addAll(node.getPartitionIds()); for(Integer partitionId: node.getPartitionIds()) { if(this.partitionIdToNodeId.containsKey(partitionId)) { throw new IllegalArgumentException("Partition id " + partitionId + " found on two nodes : " + node.getId() + " and " + this.partitionIdToNodeId.get(partitionId)); } this.partitionIdToZone.put(partitionId, nodesZone); partitionIdToNodeMap.put(partitionId, node); this.partitionIdToNode.put(partitionId, node); this.partitionIdToNodeId.put(partitionId, node.getId()); } } this.numberOfPartitionIds = getNumberOfPartitions(nodes); this.partitionIdToNodeArray = new Node[this.numberOfPartitionIds]; for(int partitionId = 0; partitionId < this.numberOfPartitionIds; partitionId++) { this.partitionIdToNodeArray[partitionId] = partitionIdToNodeMap.get(partitionId); } nodesShuffled = new ArrayList<Node>(nodesById.values()); Collections.shuffle(nodesShuffled, new Random()); } private int getNumberOfPartitions(List<Node> nodes) { List<Integer> tags = new ArrayList<Integer>(); for(Node node: nodes) { tags.addAll(node.getPartitionIds()); } Collections.sort(tags); for(int i = 0; i < tags.size(); i++) { if(tags.get(i).intValue() != i) throw new IllegalArgumentException("Invalid partition assignment. " + "The partitionId space defined in cluster.xml must be dense."); } return tags.size(); } @JmxGetter(name = "name", description = "The name of the cluster") public String getName() { return name; } public Collection<Node> getNodes() { return nodesById.values(); } public Collection<Node> getNodesShuffled() { return nodesShuffled; } /** * @return Sorted set of node Ids */ public Set<Integer> getNodeIds() { Set<Integer> nodeIds = nodesById.keySet(); return new TreeSet<Integer>(nodeIds); } /** * * @return Sorted set of Zone Ids */ public Set<Integer> getZoneIds() { Set<Integer> zoneIds = zonesById.keySet(); return new TreeSet<Integer>(zoneIds); } public Collection<Zone> getZones() { return zonesById.values(); } public Zone getZoneById(int id) { Zone zone = zonesById.get(id); if(zone == null) { throw new VoldemortException("No such zone in cluster: " + id + " Available zones : " + displayZones()); } return zone; } private String displayZones() { String zoneIDS = "{"; for(Zone z: this.getZones()) { if(zoneIDS.length() != 1) zoneIDS += ","; zoneIDS += z.getId(); } zoneIDS += "}"; return zoneIDS; } public int getNumberOfZones() { return zonesById.size(); } public int getNumberOfPartitionsInZone(Integer zoneId) { return partitionsPerZone.get(getZoneById(zoneId)).size(); } public int getNumberOfNodesInZone(Integer zoneId) { return nodesPerZone.get(getZoneById(zoneId)).size(); } /** * @return Sorted set of node Ids for given zone */ public Set<Integer> getNodeIdsInZone(Integer zoneId) { return new TreeSet<Integer>(nodesPerZone.get(getZoneById(zoneId))); } /** * @return Sorted set of partition Ids for given zone */ public Set<Integer> getPartitionIdsInZone(Integer zoneId) { return new TreeSet<Integer>(partitionsPerZone.get(getZoneById(zoneId))); } public Zone getZoneForPartitionId(int partitionId) { return partitionIdToZone.get(partitionId); } public Node getNodeForPartitionId(int partitionId) { return this.partitionIdToNodeArray[partitionId]; } public Node[] getPartitionIdToNodeArray() { return this.partitionIdToNodeArray; } /** * * @return Map of partition id to node id. */ public Map<Integer, Integer> getPartitionIdToNodeIdMap() { return new HashMap<Integer, Integer>(partitionIdToNodeId); } public Node getNodeById(int id) { Node node = nodesById.get(id); if(node == null) throw new VoldemortException("No such node in cluster: " + id); return node; } /** * Given a cluster and a node id checks if the node exists * * @param nodeId The node id to search for * @return True if cluster contains the node id, else false */ public boolean hasNodeWithId(int nodeId) { Node node = nodesById.get(nodeId); if(node == null) { return false; } return true; } @JmxGetter(name = "numberOfNodes", description = "The number of nodes in the cluster.") public int getNumberOfNodes() { return nodesById.size(); } public int getNumberOfPartitions() { return numberOfPartitionIds; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Cluster('"); builder.append(getName()); builder.append("', ["); for(Node n: getNodes()) { builder.append(n.toString()); builder.append('\n'); } builder.append("])"); return builder.toString(); } /** * Return a detailed string representation of the current cluster * * @param isDetailed * @return descripton of cluster */ public String toString(boolean isDetailed) { if(!isDetailed) { return toString(); } StringBuilder builder = new StringBuilder("Cluster [" + getName() + "] Nodes [" + getNumberOfNodes() + "] Zones [" + getNumberOfZones() + "] Partitions [" + getNumberOfPartitions() + "]"); builder.append(" Zone Info [" + getZones() + "]"); builder.append(" Node Info [" + getNodes() + "]"); return builder.toString(); } /** * Clones the cluster by constructing a new one with same name, partition * layout, and nodes. * * @param cluster * @return clone of Cluster cluster. */ public static Cluster cloneCluster(Cluster cluster) { // Could add a better .clone() implementation that clones the derived // data structures. The constructor invoked by this clone implementation // can be slow for large numbers of partitions. Probably faster to copy // all the maps and stuff. return new Cluster(cluster.getName(), new ArrayList<Node>(cluster.getNodes()), new ArrayList<Zone>(cluster.getZones())); /*- * Historic "clone" code being kept in case this, for some reason, was the "right" way to be doing this. ClusterMapper mapper = new ClusterMapper(); return mapper.readCluster(new StringReader(mapper.writeCluster(cluster))); */ } public List<String> getBootStrapUrls() { return getBootStrapUrls(5); } public List<String> getBootStrapUrls(int maxSize) { List<String> bootStrapUrls = new ArrayList<String>(); for(Node node: getNodes()) { if(bootStrapUrls.size() >= maxSize) { break; } String bootStrapUrl = node.getSocketUrl().toString(); bootStrapUrls.add(bootStrapUrl); } return bootStrapUrls; } @Override public boolean equals(Object second) { if(this == second) return true; if(second == null || second.getClass() != getClass()) return false; Cluster secondCluster = (Cluster) second; if(this.getZones().size() != secondCluster.getZones().size()) { return false; } if(this.getNodes().size() != secondCluster.getNodes().size()) { return false; } for(Zone zoneA: this.getZones()) { Zone zoneB; try { zoneB = secondCluster.getZoneById(zoneA.getId()); } catch(VoldemortException e) { return false; } if(zoneB == null || zoneB.getProximityList().size() != zoneA.getProximityList().size()) { return false; } for(int index = 0; index < zoneA.getProximityList().size(); index++) { if(zoneA.getProximityList().get(index) != zoneB.getProximityList().get(index)) { return false; } } } for(Node nodeA: this.getNodes()) { Node nodeB; try { nodeB = secondCluster.getNodeById(nodeA.getId()); } catch(VoldemortException e) { return false; } if(nodeA.getNumberOfPartitions() != nodeB.getNumberOfPartitions()) { return false; } if(nodeA.isEqualState(nodeB) == false) { return false; } if(!Sets.newHashSet(nodeA.getPartitionIds()) .equals(Sets.newHashSet(nodeB.getPartitionIds()))) return false; } return true; } @Override public int hashCode() { int hc = getNodes().size(); for(Node node: getNodes()) { hc ^= node.getHost().hashCode(); } return hc; } }