diff --git a/server/src/main/java/org/elasticsearch/common/collect/CopyOnWriteHashMap.java b/server/src/main/java/org/elasticsearch/common/collect/CopyOnWriteHashMap.java deleted file mode 100644 index 1ea52783c7762..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/collect/CopyOnWriteHashMap.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.common.collect; - -import org.apache.lucene.util.mutable.MutableValueInt; - -import java.lang.reflect.Array; -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Collection; -import java.util.Deque; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.stream.Stream; - -/** - * An immutable map whose writes result in a new copy of the map to be created. - * - * This is essentially a hash array mapped trie: inner nodes use a bitmap in - * order to map hashes to slots by counting ones. In case of a collision (two - * values having the same 32-bits hash), a leaf node is created which stores - * and searches for values sequentially. - * - * Reads and writes both perform in logarithmic time. Null keys and values are - * not supported. - * - * This structure might need to perform several object creations per write so - * it is better suited for work-loads that are not too write-intensive. - * - * @see the wikipedia page - */ -public final class CopyOnWriteHashMap extends AbstractMap { - - private static final int TOTAL_HASH_BITS = 32; - private static final Object[] EMPTY_ARRAY = new Object[0]; - - private static final int HASH_BITS = 6; - private static final int HASH_MASK = 0x3F; - - /** - * Return a copy of the provided map. - */ - public static CopyOnWriteHashMap copyOf(Map map) { - if (map instanceof CopyOnWriteHashMap) { - // no need to copy in that case - @SuppressWarnings("unchecked") - final CopyOnWriteHashMap cowMap = (CopyOnWriteHashMap) map; - return cowMap; - } else { - return new CopyOnWriteHashMap().copyAndPutAll(map); - } - } - - /** - * Abstraction of a node, implemented by both inner and leaf nodes. - */ - private abstract static class Node { - - /** - * Recursively get the key with the given hash. - */ - abstract V get(Object key, int hash); - - /** - * Recursively add a new entry to this node. hashBits is - * the number of bits that are still set in the hash. When this value - * reaches a number that is less than or equal to {@code 0}, a leaf - * node needs to be created since it means that a collision occurred - * on the 32 bits of the hash. - */ - abstract Node put(K key, int hash, int hashBits, V value, MutableValueInt newValue); - - /** - * Recursively remove an entry from this node. - */ - abstract Node remove(Object key, int hash); - - /** - * For the current node only, append entries that are stored on this - * node to entries and sub nodes to nodes. - */ - abstract void visit(Deque> entries, Deque> nodes); - - /** - * Whether this node stores nothing under it. - */ - abstract boolean isEmpty(); - - } - - /** - * A leaf of the tree where all hashes are equal. Values are added and retrieved in linear time. - */ - private static class Leaf extends Node { - - private final K[] keys; - private final V[] values; - - Leaf(K[] keys, V[] values) { - this.keys = keys; - this.values = values; - } - - @SuppressWarnings("unchecked") - Leaf() { - this((K[]) EMPTY_ARRAY, (V[]) EMPTY_ARRAY); - } - - @Override - boolean isEmpty() { - return keys.length == 0; - } - - @Override - void visit(Deque> entries, Deque> nodes) { - for (int i = 0; i < keys.length; ++i) { - entries.add(new AbstractMap.SimpleImmutableEntry<>(keys[i], values[i])); - } - } - - @Override - V get(Object key, int hash) { - for (int i = 0; i < keys.length; i++) { - if (key.equals(keys[i])) { - return values[i]; - } - } - return null; - - } - - private static T[] replace(T[] array, int index, T value) { - final T[] copy = Arrays.copyOf(array, array.length); - copy[index] = value; - return copy; - } - - @Override - Leaf put(K key, int hash, int hashBits, V value, MutableValueInt newValue) { - assert hashBits <= 0 : hashBits; - int slot = -1; - for (int i = 0; i < keys.length; i++) { - if (key.equals(keys[i])) { - slot = i; - break; - } - } - - final K[] keys2; - final V[] values2; - - if (slot < 0) { - keys2 = appendElement(keys, key); - values2 = appendElement(values, value); - newValue.value = 1; - } else { - keys2 = replace(keys, slot, key); - values2 = replace(values, slot, value); - } - - return new Leaf<>(keys2, values2); - } - - @Override - Leaf remove(Object key, int hash) { - int slot = -1; - for (int i = 0; i < keys.length; i++) { - if (key.equals(keys[i])) { - slot = i; - break; - } - } - if (slot < 0) { - return this; - } - final K[] keys2 = removeArrayElement(keys, slot); - final V[] values2 = removeArrayElement(values, slot); - return new Leaf<>(keys2, values2); - } - } - - private static T[] removeArrayElement(T[] array, int index) { - @SuppressWarnings("unchecked") - final T[] result = (T[]) Array.newInstance(array.getClass().getComponentType(), array.length - 1); - System.arraycopy(array, 0, result, 0, index); - if (index < array.length - 1) { - System.arraycopy(array, index + 1, result, index, array.length - index - 1); - } - - return result; - } - - public static T[] appendElement(final T[] array, final T element) { - final T[] newArray = Arrays.copyOf(array, array.length + 1); - newArray[newArray.length - 1] = element; - return newArray; - } - - public static T[] insertElement(final T[] array, final T element, final int index) { - final T[] result = Arrays.copyOf(array, array.length + 1); - System.arraycopy(array, 0, result, 0, index); - result[index] = element; - if (index < array.length) { - System.arraycopy(array, index, result, index + 1, array.length - index); - } - return result; - } - - /** - * An inner node in this trie. Inner nodes store up to 64 key-value pairs - * and use a bitmap in order to associate hashes to them. For example, if - * an inner node contains 5 values, then 5 bits will be set in the bitmap - * and the ordinal of the bit set in this bit map will be the slot number. - * - * As a consequence, the number of slots in an inner node is equal to the - * number of one bits in the bitmap. - */ - private static class InnerNode extends Node { - - private final long mask; // the bitmap - private final K[] keys; - final Object[] subNodes; // subNodes[slot] is either a value or a sub node in case of a hash collision - - InnerNode(long mask, K[] keys, Object[] subNodes) { - this.mask = mask; - this.keys = keys; - this.subNodes = subNodes; - assert consistent(); - } - - // only used in assert - private boolean consistent() { - assert Long.bitCount(mask) == keys.length; - assert Long.bitCount(mask) == subNodes.length; - for (int i = 0; i < keys.length; ++i) { - if (subNodes[i] instanceof Node) { - assert keys[i] == null; - } else { - assert keys[i] != null; - } - } - return true; - } - - @Override - boolean isEmpty() { - return mask == 0; - } - - @SuppressWarnings("unchecked") - InnerNode() { - this(0, (K[]) EMPTY_ARRAY, EMPTY_ARRAY); - } - - @Override - void visit(Deque> entries, Deque> nodes) { - for (int i = 0; i < keys.length; ++i) { - final Object sub = subNodes[i]; - if (sub instanceof Node) { - @SuppressWarnings("unchecked") - final Node subNode = (Node) sub; - assert keys[i] == null; - nodes.add(subNode); - } else { - @SuppressWarnings("unchecked") - final V value = (V) sub; - entries.add(new AbstractMap.SimpleImmutableEntry<>(keys[i], value)); - } - } - } - - /** - * For a given hash on 6 bits, its value is set if the bitmap has a one - * at the corresponding index. - */ - private boolean exists(int hash6) { - return (mask & (1L << hash6)) != 0; - } - - /** - * For a given hash on 6 bits, the slot number is the number of one - * bits on the right of the hash6-th bit. - */ - private int slot(int hash6) { - return Long.bitCount(mask & ((1L << hash6) - 1)); - } - - @Override - V get(Object key, int hash) { - final int hash6 = hash & HASH_MASK; - if (exists(hash6) == false) { - return null; - } - final int slot = slot(hash6); - final Object sub = subNodes[slot]; - assert sub != null; - if (sub instanceof Node) { - assert keys[slot] == null; // keys don't make sense on inner nodes - @SuppressWarnings("unchecked") - final Node subNode = (Node) sub; - return subNode.get(key, hash >>> HASH_BITS); - } else { - if (keys[slot].equals(key)) { - @SuppressWarnings("unchecked") - final V v = (V) sub; - return v; - } else { - // we have an entry for this hash, but the value is different - return null; - } - } - } - - private Node newSubNode(int hashBits) { - if (hashBits <= 0) { - return new Leaf(); - } else { - return new InnerNode(); - } - } - - @SuppressWarnings("unchecked") - private InnerNode putExisting(K key, int hash, int hashBits, int slot, V value, MutableValueInt newValue) { - final K[] keys2 = Arrays.copyOf(keys, keys.length); - final Object[] subNodes2 = Arrays.copyOf(subNodes, subNodes.length); - - final Object previousValue = subNodes2[slot]; - if (previousValue instanceof Node) { - // insert recursively - assert keys[slot] == null; - subNodes2[slot] = ((Node) previousValue).put(key, hash, hashBits, value, newValue); - } else if (keys[slot].equals(key)) { - // replace the existing entry - subNodes2[slot] = value; - } else { - // hash collision - final K previousKey = keys[slot]; - final int previousHash = previousKey.hashCode() >>> (TOTAL_HASH_BITS - hashBits); - Node subNode = newSubNode(hashBits); - subNode = subNode.put(previousKey, previousHash, hashBits, (V) previousValue, newValue); - subNode = subNode.put(key, hash, hashBits, value, newValue); - keys2[slot] = null; - subNodes2[slot] = subNode; - } - return new InnerNode<>(mask, keys2, subNodes2); - } - - private InnerNode putNew(K key, int hash6, int slot, V value) { - final long mask2 = mask | (1L << hash6); - final K[] keys2 = insertElement(keys, key, slot); - final Object[] subNodes2 = insertElement(subNodes, value, slot); - return new InnerNode<>(mask2, keys2, subNodes2); - } - - @Override - InnerNode put(K key, int hash, int hashBits, V value, MutableValueInt newValue) { - final int hash6 = hash & HASH_MASK; - final int slot = slot(hash6); - - if (exists(hash6)) { - hash >>>= HASH_BITS; - hashBits -= HASH_BITS; - return putExisting(key, hash, hashBits, slot, value, newValue); - } else { - newValue.value = 1; - return putNew(key, hash6, slot, value); - } - } - - private InnerNode removeSlot(int hash6, int slot) { - final long mask2 = mask & ~(1L << hash6); - final K[] keys2 = removeArrayElement(keys, slot); - final Object[] subNodes2 = removeArrayElement(subNodes, slot); - return new InnerNode<>(mask2, keys2, subNodes2); - } - - @Override - InnerNode remove(Object key, int hash) { - final int hash6 = hash & HASH_MASK; - if (exists(hash6) == false) { - return this; - } - final int slot = slot(hash6); - final Object previousValue = subNodes[slot]; - if (previousValue instanceof Node) { - @SuppressWarnings("unchecked") - final Node subNode = (Node) previousValue; - final Node removed = subNode.remove(key, hash >>> HASH_BITS); - if (removed == subNode) { - // not in sub-nodes - return this; - } - if (removed.isEmpty()) { - return removeSlot(hash6, slot); - } - final K[] keys2 = Arrays.copyOf(keys, keys.length); - final Object[] subNodes2 = Arrays.copyOf(subNodes, subNodes.length); - subNodes2[slot] = removed; - return new InnerNode<>(mask, keys2, subNodes2); - } else if (keys[slot].equals(key)) { - // remove entry - return removeSlot(hash6, slot); - } else { - // hash collision, nothing to remove - return this; - } - } - - } - - private static class EntryIterator implements Iterator> { - - private final Deque> entries; - private final Deque> nodes; - - EntryIterator(Node node) { - entries = new ArrayDeque<>(); - nodes = new ArrayDeque<>(); - node.visit(entries, nodes); - } - - @Override - public boolean hasNext() { - return entries.isEmpty() == false || nodes.isEmpty() == false; - } - - @Override - public Map.Entry next() { - while (entries.isEmpty()) { - if (nodes.isEmpty()) { - throw new NoSuchElementException(); - } - final Node nextNode = nodes.pop(); - nextNode.visit(entries, nodes); - } - return entries.pop(); - } - - @Override - public final void remove() { - throw new UnsupportedOperationException(); - } - - } - - private final InnerNode root; - private final int size; - - /** - * Create a new empty map. - */ - public CopyOnWriteHashMap() { - this(new InnerNode(), 0); - } - - private CopyOnWriteHashMap(InnerNode root, int size) { - this.root = root; - this.size = size; - } - - @Override - public boolean containsKey(Object key) { - // works fine since null values are not supported - return get(key) != null; - } - - @Override - public V get(Object key) { - if (key == null) { - throw new IllegalArgumentException("null keys are not supported"); - } - final int hash = key.hashCode(); - return root.get(key, hash); - } - - @Override - public int size() { - assert size != 0 || root.isEmpty(); - return size; - } - - /** - * Associate key with value and return a new copy - * of the hash table. The current hash table is not modified. - */ - public CopyOnWriteHashMap copyAndPut(K key, V value) { - if (key == null) { - throw new IllegalArgumentException("null keys are not supported"); - } - if (value == null) { - throw new IllegalArgumentException("null values are not supported"); - } - final int hash = key.hashCode(); - final MutableValueInt newValue = new MutableValueInt(); - final InnerNode newRoot = root.put(key, hash, TOTAL_HASH_BITS, value, newValue); - final int newSize = size + newValue.value; - return new CopyOnWriteHashMap<>(newRoot, newSize); - } - - /** - * Same as {@link #copyAndPut(Object, Object)} but for an arbitrary number of entries. - */ - public CopyOnWriteHashMap copyAndPutAll(Map other) { - return copyAndPutAll(other.entrySet()); - } - - public CopyOnWriteHashMap copyAndPutAll(Iterable> entries) { - CopyOnWriteHashMap result = this; - for (Entry entry : entries) { - result = result.copyAndPut(entry.getKey(), entry.getValue()); - } - return result; - } - - public CopyOnWriteHashMap copyAndPutAll(Stream> entries) { - return copyAndPutAll(entries::iterator); - } - - /** - * Remove the given key from this map. The current hash table is not modified. - */ - public CopyOnWriteHashMap copyAndRemove(Object key) { - if (key == null) { - throw new IllegalArgumentException("null keys are not supported"); - } - final int hash = key.hashCode(); - final InnerNode newRoot = root.remove(key, hash); - if (root == newRoot) { - return this; - } else { - return new CopyOnWriteHashMap<>(newRoot, size - 1); - } - } - - /** - * Same as {@link #copyAndRemove(Object)} but for an arbitrary number of entries. - */ - public CopyOnWriteHashMap copyAndRemoveAll(Collection keys) { - CopyOnWriteHashMap result = this; - for (Object key : keys) { - result = result.copyAndRemove(key); - } - return result; - } - - @Override - public Set> entrySet() { - return new AbstractSet>() { - - @Override - public Iterator> iterator() { - return new EntryIterator<>(root); - } - - @Override - public boolean contains(Object o) { - if (o == null || (o instanceof Map.Entry) == false) { - return false; - } - Map.Entry entry = (java.util.Map.Entry) o; - return entry.getValue().equals(CopyOnWriteHashMap.this.get(entry.getKey())); - } - - @Override - public int size() { - return CopyOnWriteHashMap.this.size(); - } - }; - } - -} diff --git a/server/src/test/java/org/elasticsearch/common/collect/CopyOnWriteHashMapTests.java b/server/src/test/java/org/elasticsearch/common/collect/CopyOnWriteHashMapTests.java deleted file mode 100644 index 1a7f2f9608450..0000000000000 --- a/server/src/test/java/org/elasticsearch/common/collect/CopyOnWriteHashMapTests.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.common.collect; - -import org.elasticsearch.test.ESTestCase; - -import java.util.HashMap; -import java.util.Map; - -import static java.util.Collections.emptyMap; - -public class CopyOnWriteHashMapTests extends ESTestCase { - - private static class O { - - private final int value, hashCode; - - O(int value, int hashCode) { - super(); - this.value = value; - this.hashCode = hashCode; - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || (obj instanceof O) == false) { - return false; - } - return value == ((O) obj).value; - } - } - - public void testDuel() { - final int iters = scaledRandomIntBetween(2, 5); - for (int iter = 0; iter < iters; ++iter) { - final int valueBits = randomIntBetween(1, 30); - final int hashBits = randomInt(valueBits); - // we compute the total number of ops based on the bits of the hash - // since the test is much heavier when few bits are used for the hash - final int numOps = randomInt(10 + hashBits * 100); - - Map ref = new HashMap<>(); - CopyOnWriteHashMap map = new CopyOnWriteHashMap<>(); - assertEquals(ref, map); - final int hashBase = randomInt(); - for (int i = 0; i < numOps; ++i) { - final int v = randomInt(1 << valueBits); - final int h = (v & ((1 << hashBits) - 1)) ^ hashBase; - O key = new O(v, h); - - Map newRef = new HashMap<>(ref); - final CopyOnWriteHashMap newMap; - - if (randomBoolean()) { - // ADD - Integer value = v; - newRef.put(key, value); - newMap = map.copyAndPut(key, value); - } else { - // REMOVE - final Integer removed = newRef.remove(key); - newMap = map.copyAndRemove(key); - if (removed == null) { - assertSame(map, newMap); - } - } - - assertEquals(ref, map); // make sure that the old copy has not been modified - assertEquals(newRef, newMap); - assertEquals(newMap, newRef); - - ref = newRef; - map = newMap; - } - assertEquals(ref, CopyOnWriteHashMap.copyOf(ref)); - assertEquals(emptyMap(), CopyOnWriteHashMap.copyOf(ref).copyAndRemoveAll(ref.keySet())); - } - } - - public void testCollision() { - CopyOnWriteHashMap map = new CopyOnWriteHashMap<>(); - map = map.copyAndPut(new O(3, 0), 2); - assertEquals((Integer) 2, map.get(new O(3, 0))); - assertNull(map.get(new O(5, 0))); - - map = map.copyAndPut(new O(5, 0), 5); - assertEquals((Integer) 2, map.get(new O(3, 0))); - assertEquals((Integer) 5, map.get(new O(5, 0))); - - map = map.copyAndRemove(new O(3, 0)); - assertNull(map.get(new O(3, 0))); - assertEquals((Integer) 5, map.get(new O(5, 0))); - - map = map.copyAndRemove(new O(5, 0)); - assertNull(map.get(new O(3, 0))); - assertNull(map.get(new O(5, 0))); - } - - public void testUnsupportedAPIs() { - try { - new CopyOnWriteHashMap<>().put("a", "b"); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - - try { - new CopyOnWriteHashMap<>().copyAndPut("a", "b").remove("a"); - fail(); - } catch (UnsupportedOperationException e) { - // expected - } - } - - public void testUnsupportedValues() { - try { - new CopyOnWriteHashMap<>().copyAndPut("a", null); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - - try { - new CopyOnWriteHashMap<>().copyAndPut(null, "b"); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - -} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java index 2b7c8688ff8b1..0ee1da85eb990 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java @@ -26,7 +26,6 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.collect.CopyOnWriteHashMap; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.settings.Settings; @@ -258,13 +257,13 @@ void updateAutoFollowers(ClusterState followerClusterState) { this.patterns = Set.copyOf(autoFollowMetadata.getPatterns().keySet()); - final CopyOnWriteHashMap autoFollowersCopy = CopyOnWriteHashMap.copyOf(this.autoFollowers); + final Map currentAutoFollowers = Map.copyOf(this.autoFollowers); Set newRemoteClusters = autoFollowMetadata.getPatterns() .values() .stream() .filter(AutoFollowPattern::isActive) .map(AutoFollowPattern::getRemoteCluster) - .filter(remoteCluster -> autoFollowersCopy.containsKey(remoteCluster) == false) + .filter(remoteCluster -> currentAutoFollowers.containsKey(remoteCluster) == false) .collect(Collectors.toSet()); Map newAutoFollowers = Maps.newMapWithExpectedSize(newRemoteClusters.size()); @@ -344,7 +343,7 @@ public void clusterStateProcessed(ClusterState oldState, ClusterState newState) } List removedRemoteClusters = new ArrayList<>(); - for (Map.Entry entry : autoFollowersCopy.entrySet()) { + for (Map.Entry entry : currentAutoFollowers.entrySet()) { String remoteCluster = entry.getKey(); AutoFollower autoFollower = entry.getValue(); boolean exist = autoFollowMetadata.getPatterns() @@ -365,7 +364,11 @@ public void clusterStateProcessed(ClusterState oldState, ClusterState newState) } } assert assertNoOtherActiveAutoFollower(newAutoFollowers); - this.autoFollowers = autoFollowersCopy.copyAndPutAll(newAutoFollowers).copyAndRemoveAll(removedRemoteClusters); + + Map updatedFollowers = new HashMap<>(currentAutoFollowers); + updatedFollowers.putAll(newAutoFollowers); + removedRemoteClusters.forEach(updatedFollowers.keySet()::remove); + this.autoFollowers = Collections.unmodifiableMap(updatedFollowers); } private boolean assertNoOtherActiveAutoFollower(Map newAutoFollowers) {