diff --git a/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java b/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java index 082dfb2a3..17293ab4a 100755 --- a/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java +++ b/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java @@ -324,12 +324,7 @@ public class VanillaChronicleMap cxt = mapContext()) { - for (int i = 0; i < actualSegments; i++) { - cxt.initTheSegmentIndex(i); - cxt.clear(); - } - } + forEachEntry(KeyContext::remove); } public final long readValueSize(Bytes entry) { diff --git a/src/test/java/net/openhft/chronicle/map/BiMapTest.java b/src/test/java/net/openhft/chronicle/map/BiMapTest.java index b790b15c0..457054851 100644 --- a/src/test/java/net/openhft/chronicle/map/BiMapTest.java +++ b/src/test/java/net/openhft/chronicle/map/BiMapTest.java @@ -4,12 +4,145 @@ import net.openhft.chronicle.hash.Value; import org.jetbrains.annotations.NotNull; import org.junit.Test; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BiFunction; + +import static net.openhft.chronicle.hash.Value.bytesEquivalent; +import static net.openhft.chronicle.map.BiMapTest.DualLockSuccess.FAIL; +import static net.openhft.chronicle.map.BiMapTest.DualLockSuccess.SUCCESS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class BiMapTest { - static class BiMapEntryOperations implements MapEntryOperations { + enum DualLockSuccess {SUCCESS, FAIL} + + static class BiMapMethods implements MapMethods { + @Override + public void remove(MapQueryContext q, ReturnValue returnValue) { + while (true) { + q.updateLock().lock(); + try { + MapEntry entry = q.entry(); + if (entry != null) { + returnValue.returnValue(entry.value()); + if (q.remove(entry) == SUCCESS) + return; + } + } finally { + q.readLock().unlock(); + } + } + } + + @Override + public void put(MapQueryContext q, Value value, + ReturnValue returnValue) { + while (true) { + q.updateLock().lock(); + try { + MapEntry entry = q.entry(); + if (entry != null) { + throw new IllegalStateException(); + } else { + if (q.insert(q.absentEntry(), value) == SUCCESS) + return; + } + } finally { + q.readLock().unlock(); + } + } + } + + @Override + public void putIfAbsent(MapQueryContext q, Value value, + ReturnValue returnValue) { + while (true) { + try { + if (q.readLock().tryLock()) { + MapEntry entry = q.entry(); + if (entry != null) { + returnValue.returnValue(entry.value()); + return; + } + // Key is absent + q.readLock().unlock(); + } + q.updateLock().lock(); + MapEntry entry = q.entry(); + if (entry != null) { + returnValue.returnValue(entry.value()); + return; + } + // Key is absent + if (q.insert(q.absentEntry(), value) == SUCCESS) + return; + } finally { + q.readLock().unlock(); + } + } + } + + @Override + public boolean remove(MapQueryContext q, Value value) { + while (true) { + q.updateLock().lock(); + MapEntry entry = q.entry(); + try { + if (entry != null && bytesEquivalent(entry.value(), value)) { + if (q.remove(entry) == SUCCESS) { + return true; + } else { + //noinspection UnnecessaryContinue + continue; + } + } else { + return false; + } + } finally { + q.readLock().unlock(); + } + } + } + + @Override + public void acquireUsing(MapQueryContext q, + ReturnValue returnValue) { + throw new UnsupportedOperationException(); + } + + @Override + public void replace(MapQueryContext q, Value value, + ReturnValue returnValue) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(MapQueryContext q, Value oldValue, + Value newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public void compute(MapQueryContext q, + BiFunction remappingFunction, + ReturnValue returnValue) { + throw new UnsupportedOperationException(); + } + + @Override + public void merge(MapQueryContext q, Value value, + BiFunction remappingFunction, + ReturnValue returnValue) { + throw new UnsupportedOperationException(); + } + } + + static class BiMapEntryOperations implements MapEntryOperations { ChronicleMap reverse; public void setReverse(ChronicleMap reverse) { @@ -17,14 +150,19 @@ public class BiMapTest { } @Override - public Void remove(@NotNull MapEntry entry) { + public DualLockSuccess remove(@NotNull MapEntry entry) { try (ExternalMapQueryContext rq = reverse.queryContext(entry.value().get())) { - rq.updateLock().lock(); + if (!rq.updateLock().tryLock()) { + if (entry.context() instanceof MapQueryContext) + return FAIL; + throw new IllegalStateException("Concurrent modifications to reverse map " + + "during remove during iteration"); + } MapEntry reverseEntry = rq.entry(); if (reverseEntry != null) { entry.doRemove(); reverseEntry.doRemove(); - return null; + return SUCCESS; } else { throw new IllegalStateException(entry.key() + " maps to " + entry.value() + ", but in the reverse map this value is absent"); @@ -33,24 +171,27 @@ public class BiMapTest { } @Override - public Void replaceValue(@NotNull MapEntry entry, Value newValue) { + public DualLockSuccess replaceValue(@NotNull MapEntry entry, Value newValue) { throw new UnsupportedOperationException(); } @Override - public Void insert(@NotNull MapAbsentEntry absentEntry, Value value) { + public DualLockSuccess insert(@NotNull MapAbsentEntry absentEntry, + Value value) { try (ExternalMapQueryContext rq = reverse.queryContext(value.get())) { - rq.updateLock().lock(); + if (!rq.updateLock().tryLock()) + return FAIL; MapAbsentEntry reverseAbsentEntry = rq.absentEntry(); if (reverseAbsentEntry != null) { absentEntry.doInsert(value); reverseAbsentEntry.doInsert(absentEntry.absentKey()); - return null; + return SUCCESS; } else { Value reverseKey = rq.entry().value(); if (reverseKey.equals(absentEntry.absentKey())) { // recover - return null; + absentEntry.doInsert(value); + return SUCCESS; } throw new IllegalArgumentException("Try to associate " + absentEntry.absentKey() + " with " + value + ", but in the reverse " + @@ -61,36 +202,40 @@ public class BiMapTest { } @Test - public void biMapTest() { + public void biMapTest() throws InterruptedException, ExecutionException { BiMapEntryOperations biMapOps1 = new BiMapEntryOperations<>(); ChronicleMap map1 = ChronicleMapBuilder .of(Integer.class, CharSequence.class) .entries(100) + .actualSegments(1) .averageValueSize(10) .entryOperations(biMapOps1) + .mapMethods(new BiMapMethods<>()) .create(); BiMapEntryOperations biMapOps2 = new BiMapEntryOperations<>(); ChronicleMap map2 = ChronicleMapBuilder .of(CharSequence.class, Integer.class) .entries(100) + .actualSegments(1) .averageKeySize(10) .entryOperations(biMapOps2) + .mapMethods(new BiMapMethods<>()) .create(); biMapOps1.setReverse(map2); biMapOps2.setReverse(map1); map1.put(1, "1"); - assertEquals(1, map2.get("1").intValue()); + verifyBiMapConsistent(map1, map2); map2.remove("1"); assertTrue(map2.isEmpty()); - assertTrue(map1.isEmpty()); - + verifyBiMapConsistent(map1, map2); + map1.put(3, "4"); map2.put("5", 6); - assertEquals(2, map1.size()); + verifyBiMapConsistent(map1, map2); try (ExternalMapQueryContext q = map2.queryContext("4")) { q.updateLock().lock(); @@ -112,5 +257,37 @@ public class BiMapTest { } map2.put("4", 3); // recover + verifyBiMapConsistent(map1, map2); + + map1.clear(); + verifyBiMapConsistent(map1, map2); + + ForkJoinPool pool = new ForkJoinPool(8); + pool.submit(() -> { + ThreadLocalRandom.current().ints().limit(10_000).parallel().forEach(i -> { + int v = Math.abs(i % 10); + if ((i & 1) == 0) { + if ((i & 2) == 0) { + map1.putIfAbsent(v, "" + v); + } else { + map1.remove(v, "" + v); + } + } else { + if ((i & 2) == 0) { + map2.putIfAbsent("" + v, v); + } else { + map2.remove("" + v, v); + } + } + }); + }).get(); + verifyBiMapConsistent(map1, map2); + } + + private static void verifyBiMapConsistent(Map m1, Map m2) { + assertEquals(m1.size(), m2.size()); + for (Entry e : m1.entrySet()) { + assertEquals(e.getKey(), m2.get(e.getValue())); + } } }