summaryrefslogtreecommitdiffstats
path: root/vespajlib/src/test/java/com/yahoo/yolean/concurrent
diff options
context:
space:
mode:
Diffstat (limited to 'vespajlib/src/test/java/com/yahoo/yolean/concurrent')
-rw-r--r--vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java106
-rw-r--r--vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java101
-rw-r--r--vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java146
3 files changed, 353 insertions, 0 deletions
diff --git a/vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java
new file mode 100644
index 00000000000..3f2526172a9
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java
@@ -0,0 +1,106 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.yolean.concurrent;
+
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author baldersheim
+ * @since 5.2
+ */
+public class CopyOnWriteHashMapTest {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Map<String, String> map = new CopyOnWriteHashMap<>();
+ assertEquals(0, map.size());
+ assertEquals(true, map.isEmpty());
+ assertEquals(false, map.containsKey("fooKey"));
+ assertEquals(false, map.containsValue("fooVal"));
+ assertNull(map.get("fooKey"));
+ assertNull(map.remove("fooKey"));
+ assertEquals(0, map.keySet().size());
+ assertEquals(0, map.entrySet().size());
+ assertEquals(0, map.values().size());
+
+ map.put("fooKey", "fooVal");
+ assertEquals(1, map.size());
+ assertEquals(false, map.isEmpty());
+ assertEquals(true, map.containsKey("fooKey"));
+ assertEquals(true, map.containsValue("fooVal"));
+ assertEquals("fooVal", map.get("fooKey"));
+ assertEquals(1, map.keySet().size());
+ assertEquals(1, map.entrySet().size());
+ assertEquals(1, map.values().size());
+
+ map.put("barKey", "barVal");
+ assertEquals(2, map.size());
+ assertEquals(false, map.isEmpty());
+ assertEquals(true, map.containsKey("fooKey"));
+ assertEquals(true, map.containsKey("barKey"));
+ assertEquals(true, map.containsValue("fooVal"));
+ assertEquals(true, map.containsValue("barVal"));
+ assertEquals("fooVal", map.get("fooKey"));
+ assertEquals("barVal", map.get("barKey"));
+ assertEquals(2, map.keySet().size());
+ assertEquals(2, map.entrySet().size());
+ assertEquals(2, map.values().size());
+
+ assertEquals("fooVal", map.remove("fooKey"));
+ assertEquals(1, map.size());
+ assertEquals(false, map.isEmpty());
+ assertEquals(false, map.containsKey("fooKey"));
+ assertEquals(true, map.containsKey("barKey"));
+ assertEquals(false, map.containsValue("fooVal"));
+ assertEquals(true, map.containsValue("barVal"));
+ assertNull(map.get("fooKey"));
+ assertEquals("barVal", map.get("barKey"));
+ assertEquals(1, map.keySet().size());
+ assertEquals(1, map.entrySet().size());
+ assertEquals(1, map.values().size());
+ }
+
+ @Test
+ public void requireThatEntrySetDoesNotReflectConcurrentModifications() {
+ Map<String, String> map = new CopyOnWriteHashMap<>();
+ map.put("fooKey", "fooVal");
+
+ Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
+ assertEquals("fooVal", map.remove("fooKey"));
+
+ assertTrue(it.hasNext());
+ Map.Entry<String, String> entry = it.next();
+ assertEquals("fooKey", entry.getKey());
+ assertEquals("fooVal", entry.getValue());
+ }
+
+ @Test
+ public void requireThatKeySetDoesNotReflectConcurrentModifications() {
+ Map<String, String> map = new CopyOnWriteHashMap<>();
+ map.put("fooKey", "fooVal");
+
+ Iterator<String> it = map.keySet().iterator();
+ assertEquals("fooVal", map.remove("fooKey"));
+
+ assertTrue(it.hasNext());
+ assertEquals("fooKey", it.next());
+ }
+
+ @Test
+ public void requireThatValuesDoNotReflectConcurrentModifications() {
+ Map<String, String> map = new CopyOnWriteHashMap<>();
+ map.put("fooKey", "fooVal");
+
+ Iterator<String> it = map.values().iterator();
+ assertEquals("fooVal", map.remove("fooKey"));
+
+ assertTrue(it.hasNext());
+ assertEquals("fooVal", it.next());
+ }
+}
diff --git a/vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java
new file mode 100644
index 00000000000..7f2f49c75f2
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/MemoizedTest.java
@@ -0,0 +1,101 @@
+package com.yahoo.yolean.concurrent;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+/**
+ * @author jonmv
+ */
+public class MemoizedTest {
+
+ final Phaser phaser = new Phaser();
+ final int threads = 128;
+
+ @Test
+ public void test() throws ExecutionException, InterruptedException {
+ var lazy = new Memoized<>(new OnceSupplier(), OnceCloseable::close);
+ phaser.register(); // test thread
+ phaser.register(); // whoever calls the factory
+
+ Phaser latch = new Phaser(threads + 1);
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+ List<Future<?>> futures = new ArrayList<>();
+ for (int i = 0; i < 128; i++) {
+ futures.add(executor.submit(() -> {
+ latch.arriveAndAwaitAdvance();
+ lazy.get().rendezvous();
+ while (true) lazy.get();
+ }));
+ }
+
+ // All threads waiting for latch, will race to factory
+ latch.arriveAndAwaitAdvance();
+
+ // One thread waiting in factory, the others are blocked, will go to rendezvous
+ phaser.arriveAndAwaitAdvance();
+
+ // All threads waiting in rendezvous, will repeatedly get until failure
+ phaser.arriveAndAwaitAdvance();
+
+ // Unsynchronized close should be detected by all threads
+ lazy.close();
+
+ // Close should carry through only once
+ lazy.close();
+
+ assertEquals("already closed",
+ assertThrows(IllegalStateException.class, lazy::get).getMessage());
+
+ for (Future<?> future : futures)
+ assertEquals("java.lang.IllegalStateException: already closed",
+ assertThrows(ExecutionException.class, future::get).getMessage());
+
+ executor.shutdown();
+ }
+
+ @Test
+ public void closeBeforeFirstGet() throws Exception {
+ OnceSupplier supplier = new OnceSupplier();
+ Memoized<OnceCloseable, ?> lazy = Memoized.of(supplier);
+ lazy.close();
+ assertEquals("already closed",
+ assertThrows(IllegalStateException.class, lazy::get).getMessage());
+ lazy.close();
+ assertFalse(supplier.initialized.get());
+ }
+
+ class OnceSupplier implements Supplier<OnceCloseable> {
+ final AtomicBoolean initialized = new AtomicBoolean();
+ @Override public OnceCloseable get() {
+ phaser.arriveAndAwaitAdvance();
+ if ( ! initialized.compareAndSet(false, true)) fail("initialized more than once");
+ phaser.bulkRegister(threads - 1); // register all the threads who didn't get the factory
+ return new OnceCloseable();
+ }
+ }
+
+ class OnceCloseable implements AutoCloseable {
+ final AtomicBoolean closed = new AtomicBoolean();
+ @Override public void close() {
+ if ( ! closed.compareAndSet(false, true)) fail("closed more than once");
+ }
+ void rendezvous() {
+ phaser.arriveAndAwaitAdvance();
+ }
+ }
+
+}
diff --git a/vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java
new file mode 100644
index 00000000000..c2edaf1fb00
--- /dev/null
+++ b/vespajlib/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java
@@ -0,0 +1,146 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.yolean.concurrent;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ThreadRobustListTestCase {
+
+ private final static int NUM_THREADS = 64;
+ private final static int NUM_ITEMS_TO_WRITE = 1000000;
+ private final static int NUM_TIMES_TO_READ = 10;
+
+ @Test
+ public void requireThatListIsThreadRobust() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(NUM_THREADS);
+ final ThreadRobustList<Integer> sharedList = new ThreadRobustList<>();
+
+ List<Callable<Boolean>> tasks = new ArrayList<>(NUM_THREADS);
+ tasks.add(new WriterTask(latch, sharedList));
+ for (int i = 1; i < NUM_THREADS; ++i) {
+ tasks.add(new ReaderTask(latch, sharedList));
+ }
+ for (Future<Boolean> result : Executors.newFixedThreadPool(NUM_THREADS).invokeAll(tasks)) {
+ assertTrue(result.get(60, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ public void requireThatAccessorsWork() {
+ ThreadRobustList<Object> lst = new ThreadRobustList<>();
+ assertTrue(lst.isEmpty());
+ assertFalse(lst.iterator().hasNext());
+
+ Object foo = new Object();
+ lst.add(foo);
+ assertFalse(lst.isEmpty());
+ Iterator<Object> it = lst.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ assertSame(foo, it.next());
+ assertFalse(it.hasNext());
+
+ Object bar = new Object();
+ lst.add(bar);
+ assertFalse(lst.isEmpty());
+ assertNotNull(it = lst.iterator());
+ assertTrue(it.hasNext());
+ assertSame(foo, it.next());
+ assertTrue(it.hasNext());
+ assertSame(bar, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatIteratorNextThrowsNoSuchElementExceptionWhenDone() {
+ ThreadRobustList<Object> lst = new ThreadRobustList<>();
+ Iterator<Object> it = lst.iterator();
+ assertFalse(it.hasNext());
+ try {
+ it.next();
+ fail();
+ } catch (NoSuchElementException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatIteratorRemoveIsNotSupported() {
+ ThreadRobustList<Object> lst = new ThreadRobustList<>();
+ Object obj = new Object();
+ lst.add(obj);
+ Iterator<Object> it = lst.iterator();
+ assertTrue(it.hasNext());
+ assertSame(obj, it.next());
+ try {
+ it.remove();
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ private static class WriterTask implements Callable<Boolean> {
+
+ final CountDownLatch latch;
+ final ThreadRobustList<Integer> sharedList;
+
+ WriterTask(CountDownLatch latch, ThreadRobustList<Integer> sharedList) {
+ this.latch = latch;
+ this.sharedList = sharedList;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ latch.countDown();
+ assertTrue(latch.await(60, TimeUnit.SECONDS));
+ for (int i = 0; i < NUM_ITEMS_TO_WRITE; ++i) {
+ sharedList.add(i);
+ }
+ return true;
+ }
+ }
+
+ private static class ReaderTask implements Callable<Boolean> {
+
+ final CountDownLatch latch;
+ final ThreadRobustList<Integer> sharedList;
+
+ ReaderTask(CountDownLatch latch, ThreadRobustList<Integer> sharedList) {
+ this.latch = latch;
+ this.sharedList = sharedList;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ latch.countDown();
+ assertTrue(latch.await(60, TimeUnit.SECONDS));
+ for (int i = 0; i < NUM_TIMES_TO_READ; ++i) {
+ Iterator<Integer> it = sharedList.iterator();
+ for (int j = 0; it.hasNext(); ++j) {
+ assertEquals(j, it.next().intValue());
+ }
+ }
+ return true;
+ }
+ }
+}