From 47560442cae41e35076904ec667f55caa9681d32 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Tue, 30 Jun 2020 22:46:21 -0400 Subject: [PATCH] Remove SyncMap for now, use more obvious and correct MapMaker --- .../proxy/util/bossbar/BossBarManager.java | 12 +- .../util/collect/concurrent/SyncMap.java | 233 ------- .../util/collect/concurrent/SyncMapImpl.java | 562 ----------------- .../util/collect/concurrent/SyncMapTest.java | 586 ------------------ 4 files changed, 7 insertions(+), 1386 deletions(-) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMap.java delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapImpl.java delete mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapTest.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/BossBarManager.java b/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/BossBarManager.java index ffc2e35f2..1ad600812 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/BossBarManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/BossBarManager.java @@ -1,14 +1,15 @@ package com.velocitypowered.proxy.util.bossbar; +import com.google.common.collect.MapMaker; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.util.collect.Enum2IntMap; -import com.velocitypowered.proxy.util.collect.concurrent.SyncMap; import com.velocitypowered.proxy.util.concurrent.Once; +import java.util.Collections; +import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.WeakHashMap; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.bossbar.BossBar.Color; import net.kyori.adventure.bossbar.BossBar.Flag; @@ -45,10 +46,10 @@ public class BossBarManager implements BossBar.Listener { .put(Flag.PLAY_BOSS_MUSIC, 0x2) .put(Flag.CREATE_WORLD_FOG, 0x4) .build(); - private final SyncMap bars; + private final Map bars; public BossBarManager() { - this.bars = SyncMap.of(WeakHashMap::new, 16); + this.bars = new MapMaker().weakKeys().makeMap(); } private @Nullable BossBarHolder getHandler(BossBar bar) { @@ -174,7 +175,8 @@ public class BossBarManager implements BossBar.Listener { private class BossBarHolder { private final UUID id = UUID.randomUUID(); private final BossBar bar; - private final Set subscribers = SyncMap.setOf(WeakHashMap::new, 16); + private final Set subscribers = Collections.newSetFromMap( + new MapMaker().weakKeys().makeMap()); private final Once registrationOnce = new Once(); BossBarHolder(BossBar bar) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMap.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMap.java deleted file mode 100644 index 18068a51b..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMap.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.velocitypowered.proxy.util.collect.concurrent; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; - -/** - * A concurrent map, internally backed by a non-thread-safe map but carefully managed in a matter - * such that any changes are thread-safe. Internally, the map is split into a {@code read} and a - * {@code dirty} map. The read map only satisfies read requests, while the dirty map satisfies all - * other requests. - * - *

The map is optimized for two common use cases:

- * - *
    - *
  • The entry for the given map is only written once but read many - * times, as in a cache that only grows.
  • - * - *
  • Heavy concurrent modification of entries for a disjoint set of - * keys.
  • - *
- * - *

In both cases, this map significantly reduces lock contention compared - * to a traditional map paired with a read and write lock, along with maps - * with an exclusive lock (such as with {@link Collections#synchronizedMap(Map)}.

- * - *

Null values are not accepted. Null keys are supported if the backing collection - * supports them.

- * - *

Based on: https://golang.org/src/sync/map.go

- * - * @param the key type - * @param the value type - */ -public interface SyncMap extends ConcurrentMap { - - /** - * Creates a sync map, backed by a {@link HashMap}. - * - * @param the key type - * @param the value type - * @return a sync map - */ - static SyncMap hashmap() { - return of(HashMap>::new, 16); - } - - /** - * Creates a sync map, backed by a {@link HashMap} with a provided initial capacity. - * - * @param initialCapacity the initial capacity of the hash map - * @param the key type - * @param the value type - * @return a sync map - */ - static SyncMap hashmap(final int initialCapacity) { - return of(HashMap>::new, initialCapacity); - } - - /** - * Creates a mutable set view of a sync map, backed by a {@link HashMap}. - * - * @param the key type - * @return a mutable set view of a sync map - */ - static Set hashset() { - return setOf(HashMap>::new, 16); - } - - /** - * Creates a mutable set view of a sync map, backed by a {@link HashMap} with a provided initial - * capacity. - * - * @param initialCapacity the initial capacity of the hash map - * @param the key type - * @return a mutable set view of a sync map - */ - static Set hashset(final int initialCapacity) { - return setOf(HashMap>::new, initialCapacity); - } - - /** - * Creates a sync map, backed by the provided {@link Map} implementation with a provided initial - * capacity. - * - * @param function the map creation function - * @param initialCapacity the map initial capacity - * @param the key type - * @param the value type - * @return a sync map - */ - static SyncMap of(final Function>> function, - final int initialCapacity) { - return new SyncMapImpl<>(function, initialCapacity); - } - - /** - * Creates a mutable set view of a sync map, backed by the provided {@link Map} implementation - * with a provided initial capacity. - * - * @param function the map creation function - * @param initialCapacity the map initial capacity - * @param they key type - * @return a mutable set view of a sync map - */ - static Set setOf(final Function>> function, - final int initialCapacity) { - return Collections.newSetFromMap(new SyncMapImpl<>(function, initialCapacity)); - } - - /** - * {@inheritDoc} - *

Iterations over a sync map are thread-safe, and the keys iterated over will not change for a - * single iteration attempt, however they may not necessarily reflect the state of the map at the - * time the iterator was created.

- * - *

Performance note: if entries have been appended to the map, iterating over the entry set - * will automatically promote them to the read map.

- */ - @Override - Set> entrySet(); - - /** - * {@inheritDoc} - *

This implementation is {@code O(n)} in nature due to the need to check for any expunged - * entries. Likewise, as with other concurrent collections, the value obtained by this method may - * be out of date by the time this method returns.

- * - * @return the size of all the mappings contained in this map - */ - @Override - int size(); - - /** - * {@inheritDoc} - *

- * This method clears the map by resetting the internal state to a state similar to as if a new - * map had been created. If there are concurrent iterations in progress, they will reflect the - * state of the map prior to being cleared. - *

- */ - @Override - void clear(); - - /** - * The expunging value the backing map wraps for its values. - * - * @param the backing value type - */ - interface ExpungingValue { - - /** - * Returns the backing element, which may be {@code null} if it has been expunged. - * - * @return the backing element if it has not been expunged - */ - V get(); - - /** - * Attempts to place the entry in the map if it is absent. - * - * @param value the value to place in the map - * @return a {@link Entry} with false key and null value if the value was expunged, a true key - * and the previous value in the map otherwise - */ - Entry putIfAbsent(V value); - - /** - * Returns {@code true} if this element has been expunged. - * - * @return whether or not this element has been expunged - */ - boolean isExpunged(); - - /** - * Returns {@code true} if this element has a value (it is neither expunged nor {@code null}. - * - * @return whether or not this element has a value - */ - boolean exists(); - - /** - * Sets the backing element, which can be set to {@code null}. - * - * @param element the backing element - * @return the previous element stored, or {@code null} if the entry had been expunged - */ - V set(final V element); - - /** - * Tries to replace the backing element, which can be set to {@code null}. This operation has no - * effect if the entry was expunged. - * - * @param expected the element to check for - * @param newElement the new element to be stored - * @return {@code true} if successful, {@code false} otherwise - */ - boolean replace(final Object expected, final V newElement); - - /** - * Clears the entry stored in this value. Has no effect if {@code null} is stored in the map or - * the entry was expunged. - */ - V clear(); - - /** - * Tries to set the backing element. If the entry is expunged, this operation will fail. - * - * @param element the new element - * @return {@code true} if the entry was not expunged, {@code false} otherwise - */ - boolean trySet(final V element); - - /** - * Tries to mark the item as expunged, if its value is {@code null}. - * - * @return whether or not the item has been expunged - */ - boolean tryMarkExpunged(); - - /** - * Tries to set the backing element, which can be set to {@code null}, if the entry was - * expunged. - * - * @param element the new element - * @return {@code true} if the entry was unexpunged, {@code false} otherwise - */ - boolean tryUnexpungeAndSet(final V element); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapImpl.java deleted file mode 100644 index 30b00bcea..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapImpl.java +++ /dev/null @@ -1,562 +0,0 @@ -package com.velocitypowered.proxy.util.collect.concurrent; - -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.function.Consumer; -import java.util.function.Function; - -/* package */ final class SyncMapImpl extends AbstractMap implements SyncMap { - - private final Object lock = new Object(); - private final Function>> function; - private volatile Map> read; - private volatile boolean readAmended; - private int readMisses; - private Map> dirty; - private EntrySet entrySet; - - /* package */ SyncMapImpl(final Function>> function, - final int initialCapacity) { - this.function = function; - this.read = function.apply(initialCapacity); - } - - @Override - public int size() { - if (this.readAmended) { - synchronized (this.lock) { - if (this.readAmended) { - return this.getSize(this.dirty); - } - } - } - return this.getSize(this.read); - } - - private int getSize(Map> map) { - int size = 0; - for (ExpungingValue value : map.values()) { - if (value.exists()) { - size++; - } - } - return size; - } - - private ExpungingValue getValue(final Object key) { - ExpungingValue entry = this.read.get(key); - if (entry == null && this.readAmended) { - entry = this.getValueReadMissed(key); - } - return entry; - } - - private ExpungingValue getValueReadMissed(final Object key) { - ExpungingValue entry = null; - synchronized (this.lock) { - if (this.readAmended && (entry = this.read.get(key)) == null && this.dirty != null) { - entry = this.dirty.get(key); - this.missLocked(); - } - } - return entry; - } - - @Override - public boolean containsKey(final Object key) { - ExpungingValue entry = this.getValue(key); - return entry != null && entry.exists(); - } - - @Override - public V get(final Object key) { - ExpungingValue entry = this.getValue(key); - if (entry == null) { - return null; - } - return entry.get(); - } - - @Override - public V put(final K key, final V value) { - Objects.requireNonNull(value, "value"); - - ExpungingValue entry = this.read.get(key); - V previous = entry != null ? entry.get() : null; - if (entry != null && entry.trySet(value)) { - return previous; - } - return this.putDirty(key, value, false); - } - - private V putDirty(final K key, final V value, boolean onlyIfExists) { - ExpungingValue entry; - V previous = null; - synchronized (this.lock) { - if (!onlyIfExists) { - entry = this.read.get(key); - if (entry != null && entry.tryUnexpungeAndSet(value)) { - // If we had an expunged entry, then this.dirty != null and we need to insert the entry there too. - this.dirty.put(key, entry); - return null; - } - } - - if (this.dirty != null && (entry = this.dirty.get(key)) != null) { - previous = entry.set(value); - } else if (!onlyIfExists) { - if (!this.readAmended) { - this.dirtyLocked(); - this.readAmended = true; - } - assert this.dirty != null; - this.dirty.put(key, new ExpungingValueImpl<>(value)); - previous = null; - } - } - return previous; - } - - @Override - public V remove(final Object key) { - ExpungingValue entry = this.read.get(key); - if (entry == null && this.readAmended) { - synchronized (this.lock) { - if (this.readAmended && (entry = this.read.get(key)) == null && this.dirty != null) { - entry = this.dirty.remove(key); - } - } - } - return entry != null ? entry.clear() : null; - } - - @Override - public boolean remove(final Object key, final Object value) { - Objects.requireNonNull(value, "value"); - - ExpungingValue entry = this.read.get(key); - boolean absent = entry == null; - if (absent && this.readAmended) { - synchronized (this.lock) { - if (this.readAmended && (absent = (entry = this.read.get(key)) == null) - && this.dirty != null) { - absent = (entry = this.dirty.get(key)) == null; - if (!absent && entry.replace(value, null)) { - this.dirty.remove(key); - return true; - } - } - } - } - if (!absent) { - entry.replace(value, null); - } - return false; - } - - @Override - public V putIfAbsent(K key, V value) { - Objects.requireNonNull(value, "value"); - - // Go in for a clean hit if we can. - ExpungingValue entry = this.read.get(key); - if (entry != null) { - Entry result = entry.putIfAbsent(value); - if (result.getKey() == Boolean.TRUE) { - return result.getValue(); - } - } - - synchronized (this.lock) { - entry = this.read.get(key); - if (entry != null && entry.tryUnexpungeAndSet(value)) { - this.dirty.put(key, entry); - return null; - } else if (this.dirty != null && (entry = this.dirty.get(key)) != null) { - Entry result = entry.putIfAbsent(value); - this.missLocked(); - - // The only time an entry would be expunged is if it were in the read map, and we've already checked for - // that earlier. - assert result.getKey() == Boolean.TRUE; - return result.getValue(); - } else { - if (!this.readAmended) { - this.dirtyLocked(); - this.readAmended = true; - } - assert this.dirty != null; - this.dirty.put(key, new ExpungingValueImpl<>(value)); - return null; - } - } - } - - @Override - public V replace(K key, V value) { - Objects.requireNonNull(value, "value"); - - ExpungingValue entry = this.read.get(key); - V previous = entry != null ? entry.get() : null; - if (entry != null && entry.trySet(value)) { - return previous; - } - return this.putDirty(key, value, true); - } - - @Override - public boolean replace(K key, V oldValue, V newValue) { - Objects.requireNonNull(oldValue, "oldValue"); - Objects.requireNonNull(newValue, "newValue"); - - // Try a clean hit - ExpungingValue entry = this.read.get(key); - if (entry != null && entry.replace(oldValue, newValue)) { - return true; - } - - // Failed, go to the slow path. This is considerably simpler than the others that need to consider expunging. - synchronized (this.lock) { - if (this.dirty != null) { - entry = this.dirty.get(key); - if (entry.replace(oldValue, newValue)) { - return true; - } - } - } - - return false; - } - - @Override - public void clear() { - synchronized (this.lock) { - this.read = this.function.apply(16); - this.dirty = null; - this.readMisses = 0; - this.readAmended = false; - } - } - - @Override - public Set> entrySet() { - if (this.entrySet != null) { - return this.entrySet; - } - return this.entrySet = new EntrySet(); - } - - private void promoteIfNeeded() { - if (this.readAmended) { - synchronized (this.lock) { - if (this.readAmended && this.dirty != null) { - this.promoteLocked(); - } - } - } - } - - private void promoteLocked() { - if (this.dirty != null) { - this.read = this.dirty; - } - this.dirty = null; - this.readMisses = 0; - this.readAmended = false; - } - - private void missLocked() { - this.readMisses++; - int length = this.dirty != null ? this.dirty.size() : 0; - if (this.readMisses > length) { - this.promoteLocked(); - } - } - - private void dirtyLocked() { - if (this.dirty == null) { - this.dirty = this.function.apply(this.read.size()); - for (final Entry> entry : this.read.entrySet()) { - if (!entry.getValue().tryMarkExpunged()) { - this.dirty.put(entry.getKey(), entry.getValue()); - } - } - } - } - - private static class ExpungingValueImpl implements SyncMap.ExpungingValue { - - /** - * A marker object used to indicate that the value in this map has been expunged. - */ - private static final Object EXPUNGED = new Object(); - - // The raw type is required here, which is sad, but type erasure has forced our hand in this - // regard. (Besides, using an Object type and casting to the desired value allows us to reuse - // this field to see if the value has been expunged.) - // - // Type-safety is ensured by ensuring the special EXPUNGED value is never returned and using - // generics on the set and constructor calls. - private static final AtomicReferenceFieldUpdater valueUpdater = - AtomicReferenceFieldUpdater.newUpdater(ExpungingValueImpl.class, Object.class, "value"); - - private volatile Object value; - - private ExpungingValueImpl(final V value) { - this.value = value; - } - - @Override - public V get() { - Object value = valueUpdater.get(this); - if (value == EXPUNGED) { - return null; - } - return (V) value; - } - - @Override - public Entry putIfAbsent(V value) { - for (; ; ) { - Object existingVal = valueUpdater.get(this); - if (existingVal == EXPUNGED) { - return new SimpleImmutableEntry<>(Boolean.FALSE, null); - } - - if (existingVal != null) { - return new SimpleImmutableEntry<>(Boolean.TRUE, (V) existingVal); - } - - if (valueUpdater.compareAndSet(this, null, value)) { - return new SimpleImmutableEntry<>(Boolean.TRUE, null); - } - } - } - - @Override - public boolean isExpunged() { - return valueUpdater.get(this) == EXPUNGED; - } - - @Override - public boolean exists() { - Object val = valueUpdater.get(this); - return val != null && val != EXPUNGED; - } - - @Override - public V set(final V value) { - Object oldValue = valueUpdater.getAndSet(this, value); - return oldValue == EXPUNGED ? null : (V) oldValue; - } - - @Override - public boolean trySet(final V newValue) { - for (; ; ) { - Object foundValue = valueUpdater.get(this); - if (foundValue == EXPUNGED) { - return false; - } - - if (valueUpdater.compareAndSet(this, foundValue, newValue)) { - return true; - } - } - } - - @Override - public boolean tryMarkExpunged() { - Object val = valueUpdater.get(this); - while (val == null) { - if (valueUpdater.compareAndSet(this, null, EXPUNGED)) { - return true; - } - val = valueUpdater.get(this); - } - return false; - } - - @Override - public boolean tryUnexpungeAndSet(V element) { - return valueUpdater.compareAndSet(this, EXPUNGED, element); - } - - @Override - public boolean replace(final Object expected, final V newValue) { - for (; ; ) { - Object val = valueUpdater.get(this); - if (val == EXPUNGED || !Objects.equals(val, expected)) { - return false; - } - - if (valueUpdater.compareAndSet(this, val, newValue)) { - return true; - } - } - } - - @Override - public V clear() { - for (; ; ) { - Object val = valueUpdater.get(this); - if (val == null || val == EXPUNGED) { - return null; - } - if (valueUpdater.compareAndSet(this, val, null)) { - return (V) val; - } - } - } - } - - private class MapEntry implements Entry { - - private final K key; - - private MapEntry(final Entry> entry) { - this.key = entry.getKey(); - } - - @Override - public K getKey() { - return this.key; - } - - @Override - public V getValue() { - return SyncMapImpl.this.get(this.key); - } - - @Override - public V setValue(final V value) { - return SyncMapImpl.this.put(this.key, value); - } - - @Override - public String toString() { - return "SyncMapImpl.MapEntry{key=" + this.getKey() + ", value=" + this.getValue() + "}"; - } - - @Override - public boolean equals(final Object other) { - if (this == other) { - return true; - } - if (!(other instanceof Map.Entry)) { - return false; - } - final Entry that = (Entry) other; - return Objects.equals(this.getKey(), that.getKey()) - && Objects.equals(this.getValue(), that.getValue()); - } - - @Override - public int hashCode() { - return Objects.hash(this.getKey(), this.getValue()); - } - } - - private class EntrySet extends AbstractSet> { - - @Override - public int size() { - return SyncMapImpl.this.size(); - } - - @Override - public boolean contains(final Object entry) { - if (!(entry instanceof Map.Entry)) { - return false; - } - final Entry mapEntry = (Entry) entry; - final V value = SyncMapImpl.this.get(mapEntry.getKey()); - return value != null && Objects.equals(mapEntry.getValue(), value); - } - - @Override - public boolean remove(final Object entry) { - if (!(entry instanceof Map.Entry)) { - return false; - } - final Entry mapEntry = (Entry) entry; - return SyncMapImpl.this.remove(mapEntry.getKey()) != null; - } - - @Override - public void clear() { - SyncMapImpl.this.clear(); - } - - @Override - public Iterator> iterator() { - SyncMapImpl.this.promoteIfNeeded(); - return new EntryIterator(SyncMapImpl.this.read.entrySet().iterator()); - } - } - - private class EntryIterator implements Iterator> { - - private final Iterator>> backingIterator; - private Entry next; - private Entry current; - - private EntryIterator(final Iterator>> backingIterator) { - this.backingIterator = backingIterator; - Entry> entry = this.getNextValue(); - this.next = (entry != null ? new MapEntry(entry) : null); - } - - private Entry> getNextValue() { - Entry> entry = null; - while (this.backingIterator.hasNext() && entry == null) { - final ExpungingValue value = (entry = this.backingIterator.next()).getValue(); - if (!value.exists()) { - entry = null; - } - } - return entry; - } - - @Override - public boolean hasNext() { - return this.next != null; - } - - @Override - public Entry next() { - this.current = this.next; - Entry> entry = this.getNextValue(); - this.next = (entry != null ? new MapEntry(entry) : null); - if (this.current == null) { - throw new NoSuchElementException(); - } - return this.current; - } - - @Override - public void remove() { - if (this.current == null) { - return; - } - SyncMapImpl.this.remove(this.current.getKey()); - } - - @Override - public void forEachRemaining(final Consumer> action) { - if (this.next != null) { - action.accept(this.next); - } - this.backingIterator.forEachRemaining(entry -> { - if (entry.getValue().exists()) { - action.accept(new MapEntry(entry)); - } - }); - } - } -} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapTest.java deleted file mode 100644 index f98b21d92..000000000 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/concurrent/SyncMapTest.java +++ /dev/null @@ -1,586 +0,0 @@ -package com.velocitypowered.proxy.util.collect.concurrent; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.velocitypowered.proxy.util.collect.concurrent.SyncMap.ExpungingValue; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.Spliterator; -import org.junit.jupiter.api.Test; - -public class SyncMapTest { - @Test - public void testInitialization() { - final SyncMap map = SyncMap.hashmap(); - assertEquals(0, map.size()); - assertTrue(map.isEmpty()); - assertFalse(map.containsKey("foo")); - assertFalse(map.containsValue("bar")); - assertNull(map.get("foo")); - assertNull(map.remove("foo")); - assertFalse(map.remove("foo", "bar")); - } - - @Test - public void testPutDisallowsNullValues() { - final SyncMap map = SyncMap.hashmap(); - assertThrows(NullPointerException.class, () -> map.put("test", null)); - } - - @Test - public void testMutation_put_get() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - assertEquals("bar", map.get("foo")); - assertEquals("bar", map.put("foo", "baz")); - assertEquals("baz", map.get("foo")); - assertEquals(1, map.size()); - assertFalse(map.isEmpty()); - assertTrue(map.containsKey("foo")); - assertTrue(map.containsValue("baz")); - } - - @Test - public void testMutation_put_getAllowsNull() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put(null, "bar")); - assertEquals("bar", map.get(null)); - } - - @Test - public void testMutation_repeatedPut() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - - // Make sure to promote the map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - assertEquals("bar", map.put("foo", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testMutation_handlesDirtyAndReadPut() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - - // Make sure to promote the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - // Add a new entry to the dirty map. The entry in the read must not be affected. - assertNull(map.put("abc", "123")); - assertEquals("123", map.get("abc")); - assertEquals("bar", map.get("foo")); - } - - @Test - public void testMutation_ensuresExpungedEntriesProperlyHandled() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - - // Make sure to promote the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - // Remove the entry in the read map. - assertEquals("bar", map.remove("foo")); - assertEquals(0, map.size()); - - // Add a new entry to the dirty map. At this point, the entry should be expunged. - assertNull(map.put("abc", "123")); - - // Make sure the entry still doesn't exist. - assertNull(map.get("foo")); - - // Promote the dirty map one last time - for (int i = 0; i < 10; i++) { - assertEquals("123", map.get("abc")); - } - - // Make sure the entry really doesn't exist. - assertNull(map.get("foo")); - } - - @Test - public void testMutation_putExpungedProperlyHandled() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - - // Make sure to promote the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - // Remove the entry in the read map. - assertEquals("bar", map.remove("foo")); - assertEquals(0, map.size()); - - // Add a new entry to the dirty map. At this point, the entry should be marked as expunged. - assertNull(map.put("abc", "123")); - - // Bring the old entry back to life. - assertNull(map.put("foo", "baz")); - assertEquals("baz", map.get("foo")); - - // Cause the dirty map to be promoted. - for (int i = 0; i < 10; i++) { - assertEquals("123", map.get("abc")); - } - - // Make sure the entries we added still exist (the dirty map was properly updated). - assertEquals("123", map.get("abc")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testPutIfAbsentDisallowsNullValues() { - final SyncMap map = SyncMap.hashmap(); - assertThrows(NullPointerException.class, () ->map.putIfAbsent("test", null)); - } - - @Test - public void testMutation_putIfAbsentBasic() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.putIfAbsent("foo", "bar")); - assertEquals("bar", map.putIfAbsent("foo", "baz")); - assertEquals("bar", map.get("foo")); - - assertEquals("bar", map.remove("foo")); - assertNull(map.putIfAbsent("foo", "bar")); - } - - @Test - public void testMutation_putIfAbsentPromoted() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.putIfAbsent("foo", "bar")); - - // Force a promotion of the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - // Remove the entry in the read map. - assertEquals("bar", map.putIfAbsent("foo", "baz")); - assertEquals("bar", map.get("foo")); - } - - @Test - public void testReplaceSingleDisallowsNullValues() { - final SyncMap map = SyncMap.hashmap(); - assertThrows(NullPointerException.class, () -> map.replace("test", null)); - } - - @Test - public void testMutation_replaceSingleBasic() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.replace("foo", "bar")); - assertEquals(0, map.size()); - - assertNull(map.put("foo", "bar")); - assertEquals("bar", map.replace("foo", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testMutation_replaceSingleHandlesPromotion() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - // Force a promotion of the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - assertEquals("bar", map.replace("foo", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testReplaceSpecificDisallowsNullValues1() { - final SyncMap map = SyncMap.hashmap(); - assertThrows(NullPointerException.class, () -> map.replace("test", null, - "abc")); - } - - @Test - public void testReplaceSpecificDisallowsNullValues2() { - final SyncMap map = SyncMap.hashmap(); - assertThrows(NullPointerException.class, () -> map.replace("test", "abc", - null)); - } - - @Test - public void testMutation_replaceSpecificBasic() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - assertFalse(map.replace("foo", "baz", "bar")); - assertEquals("bar", map.get("foo")); - assertTrue(map.replace("foo", "bar", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testMutation_replaceSpecificPromoted() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - // Force a promotion of the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - assertFalse(map.replace("foo", "baz", "bar")); - assertEquals("bar", map.get("foo")); - assertTrue(map.replace("foo", "bar", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testMutation_putIfAbsentHandleExpunged() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.putIfAbsent("foo", "bar")); - - // Force a promotion of the dirty map - for (int i = 0; i < 10; i++) { - assertEquals("bar", map.get("foo")); - } - - // Remove the entry in the read map. - assertEquals("bar", map.remove("foo")); - assertEquals(0, map.size()); - - // Add a new entry to the dirty map. At this point, the existing entry should be marked as expunged. - assertNull(map.put("abc", "123")); - - // Bring the old entry back to life - assertNull(map.putIfAbsent("foo", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testMutation_putAll() { - final SyncMap map = SyncMap.hashmap(); - final Map test = Maps.newHashMap(); - test.put("1", "2"); - test.put("3", "4"); - test.put("5", "6"); - - map.putAll(test); - assertEquals("2", map.get("1")); - assertEquals("4", map.get("3")); - assertEquals("6", map.get("5")); - } - - @Test - public void testMutation_remove() { - final SyncMap map = SyncMap.hashmap(); - map.put("foo", "bar"); - map.put("abc", "123"); - assertEquals(2, map.size()); - assertEquals("bar", map.remove("foo")); - assertTrue(map.remove("abc", "123")); - assertNull(map.get("foo")); - assertNull(map.get("abc")); - assertEquals(0, map.size()); - } - - @Test - public void testMutation_unexpunge() { - final SyncMap map = SyncMap.hashmap(); - assertNull(map.put("foo", "bar")); - assertEquals("bar", map.remove("foo")); - assertNull(map.put("foo", "baz")); - assertEquals("baz", map.get("foo")); - } - - @Test - public void testMutation_clear() { - final SyncMap map = SyncMap.hashmap(); - map.put("example", "random"); - map.clear(); - assertNull(map.get("example")); - assertEquals(0, map.size()); - assertTrue(map.isEmpty()); - } - - @Test - public void testKeySetPermittedMutations() { - final SyncMap map = SyncMap.hashmap(); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Set keys = map.keySet(); - assertEquals(3, keys.size()); - assertFalse(keys.isEmpty()); - assertTrue(keys.contains("1")); - assertFalse(keys.contains("2")); - assertTrue(keys.remove("1")); - assertFalse(keys.remove("2")); - assertFalse(keys.contains("1")); - assertEquals(2, keys.size()); - } - - @Test - public void testKeySetAdditionProhibited() { - final SyncMap map = SyncMap.hashmap(); - final Set keys = map.keySet(); - assertThrows(UnsupportedOperationException.class, () -> keys.add("foo")); - } - - @Test - public void testKeySetAddAllProhibited() { - final SyncMap map = SyncMap.hashmap(); - final Set keys = map.keySet(); - assertThrows(UnsupportedOperationException.class, () -> keys.addAll(Lists - .newArrayList("bar", "baz"))); - } - - @Test - public void testKeyMutation_iterator() { - final SyncMap map = SyncMap.of( - LinkedHashMap>::new, 3); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Set keys = map.keySet(); - final Iterator keyIterator = keys.iterator(); - assertTrue(keyIterator.hasNext()); - assertEquals("1", keyIterator.next()); - keyIterator.remove(); - assertFalse(keys.contains("1")); - - final String[] expected = {"3", "5"}; - final List remaining = new ArrayList<>(); - keyIterator.forEachRemaining(remaining::add); - assertArrayEquals(expected, remaining.toArray()); - } - - @Test - public void testKeyMutation_spliterator() { - final SyncMap map = SyncMap.of(LinkedHashMap>::new, 3); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Set keys = map.keySet(); - final Spliterator keySpliterator = keys.spliterator(); - assertTrue(keySpliterator.tryAdvance(value -> assertEquals("1", value))); - - final String[] expected = {"3", "5"}; - final List remaining = new ArrayList<>(); - keySpliterator.forEachRemaining(remaining::add); - assertArrayEquals(expected, remaining.toArray()); - - assertEquals(3, keySpliterator.estimateSize()); - } - - @Test - public void testValuesPermittedMutation() { - final SyncMap map = SyncMap.hashmap(); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Collection values = map.values(); - assertEquals(3, values.size()); - assertFalse(values.isEmpty()); - assertTrue(values.contains("2")); - assertFalse(values.contains("1")); - assertTrue(values.remove("2")); - assertFalse(values.remove("1")); - assertFalse(values.contains("2")); - assertEquals(2, values.size()); - } - - @Test - public void testValuesAdditionProhibited() { - final SyncMap map = SyncMap.hashmap(); - final Collection values = map.values(); - assertThrows(UnsupportedOperationException.class, () -> values.add("foo")); // Causes UOE - } - - @Test - public void testValuesAddAllProhibited() { - final SyncMap map = SyncMap.hashmap(); - final Collection values = map.values(); - assertThrows(UnsupportedOperationException.class, () -> values.addAll(Lists - .newArrayList("bar", "baz"))); // Causes UOE - } - - @Test - public void testValueMutation_iterator() { - final SyncMap map = SyncMap.of(LinkedHashMap>::new, 3); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Collection values = map.values(); - final Iterator valueIterator = values.iterator(); - assertTrue(valueIterator.hasNext()); - assertEquals("2", valueIterator.next()); - valueIterator.remove(); - assertFalse(values.contains("2")); - - final String[] expected = {"4", "6"}; - final List remaining = new ArrayList<>(); - valueIterator.forEachRemaining(remaining::add); - assertArrayEquals(expected, remaining.toArray()); - } - - @Test - public void testValueMutation_spliterator() { - final SyncMap map = SyncMap.of(LinkedHashMap>::new, 3); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Collection values = map.values(); - final Spliterator valueSpliterator = values.spliterator(); - assertTrue(valueSpliterator.tryAdvance(value -> assertEquals("2", value))); - - final String[] expected = {"4", "6"}; - final List remaining = new ArrayList<>(); - valueSpliterator.forEachRemaining(remaining::add); - assertArrayEquals(expected, remaining.toArray()); - - assertEquals(3, valueSpliterator.estimateSize()); - } - - @Test - public void testEntryMutation() { - final SyncMap map = SyncMap.hashmap(); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Map.Entry goodEntry = this.exampleEntry("1", "2"); - final Map.Entry badEntry = this.exampleEntry("abc", "123"); - - final Set> entries = map.entrySet(); - assertEquals(3, entries.size()); - assertFalse(entries.isEmpty()); - assertTrue(entries.contains(goodEntry)); - assertFalse(entries.contains(badEntry)); - assertTrue(entries.remove(goodEntry)); - assertFalse(entries.remove(badEntry)); - assertFalse(entries.contains(goodEntry)); - assertEquals(2, entries.size()); - } - - @Test - public void testEntryAddProhibited() { - final SyncMap map = SyncMap.hashmap(); - final Map.Entry badEntry = this.exampleEntry("abc", "123"); - assertThrows(UnsupportedOperationException.class, () -> map.entrySet().add(badEntry)); - } - - @Test - public void testEntryAddAllProhibited() { - final SyncMap map = SyncMap.hashmap(); - final Map.Entry badEntry = this.exampleEntry("abc", "123"); - final Map.Entry badEntry2 = this.exampleEntry("1", "2"); - assertThrows(UnsupportedOperationException.class, () -> map.entrySet().addAll(Lists - .newArrayList(badEntry, badEntry2))); - } - - @Test - public void testEntryMutation_iterator() { - final SyncMap map = SyncMap.of(LinkedHashMap>::new, 3); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Map.Entry firstEntry = this.exampleEntry("1", "2"); - final Map.Entry secondEntry = this.exampleEntry("3", "4"); - final Map.Entry thirdEntry = this.exampleEntry("5", "6"); - - final Set> entries = map.entrySet(); - final Iterator> entryIterator = entries.iterator(); - assertTrue(entryIterator.hasNext()); - assertEquals(entryIterator.next(), firstEntry); - entryIterator.remove(); - assertFalse(entries.contains(firstEntry)); - - final List> remaining = new ArrayList<>(); - entryIterator.forEachRemaining(remaining::add); - assertEquals(Lists.newArrayList(secondEntry, thirdEntry), remaining); - } - - @Test - public void testEntryMutation_spliterator() { - final SyncMap map = SyncMap.of(LinkedHashMap>::new, 3); - map.put("1", "2"); - map.put("3", "4"); - map.put("5", "6"); - - final Map.Entry firstEntry = this.exampleEntry("1", "2"); - final Map.Entry secondEntry = this.exampleEntry("3", "4"); - final Map.Entry thirdEntry = this.exampleEntry("5", "6"); - - final Set> entries = map.entrySet(); - final Spliterator> entrySpliterator = entries.spliterator(); - assertTrue(entrySpliterator.tryAdvance(value -> assertEquals(firstEntry, value))); - - final List> remaining = new ArrayList<>(); - entrySpliterator.forEachRemaining(remaining::add); - assertEquals(Lists.newArrayList(secondEntry, thirdEntry), remaining); - - assertEquals(3, entrySpliterator.estimateSize()); - } - - private Map.Entry exampleEntry(final String key, final String value) { - return new Map.Entry() { - @Override - public String getKey() { - return key; - } - - @Override - public String getValue() { - return value; - } - - @Override - public String setValue(String value) { - return value; - } - - @Override - public String toString() { - return "SyncMapImpl.MapEntry{key=" + this.getKey() + ", value=" + this.getValue() + "}"; - } - - @Override - public boolean equals(final Object other) { - if(this == other) return true; - if(!(other instanceof Map.Entry)) return false; - final Map.Entry that = (Map.Entry) other; - return Objects.equals(this.getKey(), that.getKey()) - && Objects.equals(this.getValue(), that.getValue()); - } - - @Override - public int hashCode() { - return Objects.hash(this.getKey(), this.getValue()); - } - }; - } -}