/* * JBoss, Home of Professional Open Source * Copyright 2010 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.distribution.ch; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import org.infinispan.commons.hash.Hash; import org.infinispan.marshall.AbstractExternalizer; import org.infinispan.remoting.transport.Address; import org.infinispan.util.Util; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import static java.lang.String.format; /** * <p> * Abstract class for the wheel-based CH implementations. * </p> * * <p> * This base class supports virtual nodes. To enable virtual nodes you must set * <code>numVirtualNodes</code> to a number > 1. * </p> * * <p> * Enabling virtual nodes means that a cache will appear multiple times on the hash * wheel. If an implementation doesn't want to support this, it should override * {@link #setNumVirtualNodes(Integer)} to throw an {@link IllegalArgumentException} * for values != 1. * </p> * * @author Mircea.Markus@jboss.com * @author Pete Muir * @author Dan Berindei <dberinde@redhat.com> * @since 4.2 */ public abstract class AbstractWheelConsistentHash extends AbstractConsistentHash { private static final Log LOG = LogFactory.getLog(AbstractWheelConsistentHash.class); protected final boolean trace; protected Hash hashFunction; protected int numVirtualNodes = 1; protected Set<Address> caches; // A map of normalized hashes -> cache addresses, represented as two arrays for performance considerations // positionKeys.length == positionValues.length == caches.size() * numVirtualNodes // positionKeys is sorted so we can search in it using binary search, see getPositionIndex(int) protected int[] positionKeys; protected Address[] positionValues; protected AbstractWheelConsistentHash() { trace = getLog().isTraceEnabled(); } public void setHashFunction(Hash h) { checkCachesUninitialized("hash function"); hashFunction = h; } private void checkCachesUninitialized(String property) { if (caches != null) { throw new IllegalStateException(format( "Must configure the %s before adding the caches", property)); } } public void setNumVirtualNodes(Integer numVirtualNodes) { checkCachesUninitialized("number of virtual nodes"); this.numVirtualNodes = numVirtualNodes; } @Override public void setCaches(Set<Address> newCaches) { if (newCaches.size() == 0 || newCaches.contains(null)) throw new IllegalArgumentException("Invalid cache list for consistent hash: " + newCaches); if (((long) newCaches.size()) * numVirtualNodes > Integer.MAX_VALUE) throw new IllegalArgumentException("Too many nodes: " + newCaches.size() + " * " + numVirtualNodes + " exceeds the available hash space"); // first find the correct position key for each node, as it may be different from its normalized hash // still, we would like the cache address to map to that cache as much as possible // so we add the virtual nodes (if any) only after we have added all the "real" nodes TreeMap<Integer, Address> positions = new TreeMap<Integer, Address>(); for (Address a : newCaches) { addNode(positions, a, getNormalizedHash(a)); } if (isVirtualNodesEnabled()) { for (Address a : newCaches) { for (int i = 1; i < numVirtualNodes; i++) { // we get the normalized hash from the VirtualAddress, but we store the real address in the positions map Address va = new VirtualAddress(a, i); addNode(positions, a, getNormalizedHash(va)); } } } Log logger = getLog(); if (logger.isDebugEnabled()) { logger.debugf("Using %d virtualNodes to initialize consistent hash wheel ", numVirtualNodes); logger.tracef("Positions are: %s", positions); } // then populate caches, positionKeys and positionValues with the correct values (and in the correct order) caches = new LinkedHashSet<Address>(newCaches.size()); positionKeys = new int[positions.size()]; positionValues = new Address[positions.size()]; int i = 0; for (Map.Entry<Integer, Address> position : positions.entrySet()) { caches.add(position.getValue()); positionKeys[i] = position.getKey(); positionValues[i] = position.getValue(); i++; } getLog().tracef("Consistent hash initialized: %s", this); } private void addNode(TreeMap<Integer, Address> positions, Address a, int positionIndex) { // this is deterministic since the address list is ordered and the order is consistent across the grid while (positions.containsKey(positionIndex)) { if (positionIndex == Integer.MAX_VALUE) positionIndex = 0; else positionIndex = positionIndex + 1; } positions.put(positionIndex, a); } @Override public final Set<Address> getCaches() { return caches; } protected final int getPositionIndex(int normalizedHash) { int index = Arrays.binarySearch(positionKeys, normalizedHash); // Arrays.binarySearch returns (-(insertion point) - 1) when the value is not found // we need (insertion point) instead if (index < 0) { index = -index - 1; if (index == positionKeys.length) index = 0; } return index; } /** * Creates an iterator over the positions "map" starting at the index specified by the <code>normalizedHash</code>. */ protected final Iterator<Address> getPositionsIterator(final int normalizedHash) { final int startIndex = getPositionIndex(normalizedHash); return new Iterator<Address>() { int i = startIndex; @Override public boolean hasNext() { return i >= 0; } @Override public Address next() { Address value = positionValues[i]; i++; // go back to the start if (i == positionKeys.length) i = 0; // we have come full cycle if (i == startIndex) i = -1; return value; } @Override public void remove() { throw new UnsupportedOperationException("The positions map cannot be modified"); } }; } @Override public final List<Integer> getHashIds(Address a) { // Not the most efficient way of doing this but it's usage it's so far // limited to the HotRod server and it does it only on once on startup, // so there's no urgency in finding a better way to implement this. // If virtual nodes are enabled, the list should be as long as // the number of virtual nodes, otherwise it's only one element. List<Integer> hashIds = null; boolean vNodesEnabled = isVirtualNodesEnabled(); for (int i = 0; i < positionValues.length; i++) { if (positionValues[i].equals(a)) { if (vNodesEnabled && hashIds == null) hashIds = new ArrayList<Integer>(numVirtualNodes); if (vNodesEnabled) hashIds.add(positionKeys[i]); else return Collections.singletonList(positionKeys[i]); } } if (hashIds == null) return Collections.emptyList(); else return hashIds; } public final int getNormalizedHash(final Object key) { return Util.getNormalizedHash(key, hashFunction); } public final boolean isVirtualNodesEnabled() { return numVirtualNodes > 1; } @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); sb.append(" {"); for (int i = 0; i < positionKeys.length; i++) { if (i > 0) { sb.append(", "); } sb.append(positionKeys[i]).append(": ").append(positionValues[i]); } sb.append("}"); return sb.toString(); } @Override public final Address primaryLocation(final Object key) { final int normalizedHash = getNormalizedHash(getGrouping(key)); return positionValues[getPositionIndex(normalizedHash)]; } protected Log getLog() { return LOG; } public static abstract class Externalizer<T extends AbstractWheelConsistentHash> extends AbstractExternalizer<T> { // Injecting a classloader here is redundant and complicates marshalling // code. Let JBoss Marshalling's class resolver do its job, which is // resolving classes sent around. protected abstract T instance(); @Override public void writeObject(ObjectOutput output, T abstractWheelConsistentHash) throws IOException { output.writeInt(abstractWheelConsistentHash.numVirtualNodes); output.writeObject(abstractWheelConsistentHash.hashFunction); output.writeObject(abstractWheelConsistentHash.caches); } @Override @SuppressWarnings("unchecked") public T readObject(ObjectInput unmarshaller) throws IOException, ClassNotFoundException { T instance = instance(); instance.numVirtualNodes = unmarshaller.readInt(); Hash hash = (Hash) unmarshaller.readObject(); instance.setHashFunction(hash); Set<Address> caches = (Set<Address>) unmarshaller.readObject(); instance.setCaches(caches); return instance; } } }