/* * Copyright 2015 Terracotta, Inc., a Software AG company. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terracotta.offheapstore; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Assert; import org.junit.Test; import org.terracotta.offheapstore.buffersource.HeapBufferSource; import org.terracotta.offheapstore.concurrent.AbstractConcurrentOffHeapCache; import org.terracotta.offheapstore.paging.UnlimitedPageSource; import org.terracotta.offheapstore.util.Generator; import org.terracotta.offheapstore.util.Generator.SpecialInteger; import java.util.Iterator; import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.core.AnyOf.anyOf; import static org.hamcrest.core.CombinableMatcher.either; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeThat; /** * * @author cdennis */ public abstract class AbstractConcurrentOffHeapMapIT extends AbstractOffHeapMapIT { public AbstractConcurrentOffHeapMapIT(Generator generator) { super(generator); } @Override protected abstract ConcurrentMap<SpecialInteger, SpecialInteger> createMap(Generator generator); @Test public void testConcurrentMethodNpeBehaviors() { ConcurrentMap<SpecialInteger, SpecialInteger> map = createMap(generator); try { map.remove(null, null); fail("Expected NullPointerException"); } catch (NullPointerException e) { //expected } try { map.replace(null, generator.generate(1)); fail("Expected NullPointerException"); } catch (NullPointerException e) { //expected } try { map.replace(null, generator.generate(1), generator.generate(1)); fail("Expected NullPointerException"); } catch (NullPointerException e) { //expected } try { map.putIfAbsent(null, generator.generate(1)); fail("Expected NullPointerException"); } catch (NullPointerException e) { //expected } } @Test public final void testConcurrentMap() { long randomSeed = System.nanoTime(); System.err.println(this.getClass() + ".testConcurrentMap random seed = " + randomSeed); Random rndm = new Random(randomSeed); ConcurrentMap<SpecialInteger, SpecialInteger> m = createMap(generator); SpecialInteger keyOne = generator.generate(rndm.nextInt()); SpecialInteger valueOne = generator.generate(rndm.nextInt()); assertThat(m.putIfAbsent(keyOne, valueOne), nullValue()); assertTrue(m.containsKey(keyOne)); assertThat(m.putIfAbsent(keyOne, generator.generate(rndm.nextInt())), is(valueOne)); assertTrue(m.containsKey(keyOne)); assertFalse(m.replace(keyOne, generator.generate(rndm.nextInt()), generator.generate(rndm.nextInt()))); assertTrue(m.containsKey(keyOne)); assertTrue(m.containsKey(keyOne)); assertThat(m.get(keyOne), is(valueOne)); SpecialInteger valueTwo = generator.generate(5555); assertTrue(m.replace(keyOne, valueOne, valueTwo)); assertTrue(m.replace(keyOne, valueTwo, valueOne)); assertThat(m.get(keyOne), is(valueOne)); SpecialInteger keyTwo; do { keyTwo = generator.generate(rndm.nextInt()); } while (keyOne.equals(keyTwo)); assertThat(m.replace(keyTwo, generator.generate(rndm.nextInt())), nullValue()); assertThat(m.replace(keyOne, valueOne), is(valueOne)); assertThat(m.replace(keyOne, valueTwo), is(valueOne)); assertThat(m.get(keyOne), is(valueTwo)); assertThat(m.replace(keyOne, valueOne), is(valueTwo)); assertThat(m.get(keyOne), is(valueOne)); SpecialInteger valueThree; do { valueThree = generator.generate(rndm.nextInt()); } while (valueOne.equals(valueThree)); assertFalse(m.remove(keyOne, valueThree)); assertThat(m.get(keyOne), is(valueOne)); assertTrue(m.containsKey(keyOne)); assertFalse(m.remove(keyOne, null)); assertThat(m.get(keyOne), is(valueOne)); assertTrue(m.remove(keyOne, valueOne)); assertThat(m.get(keyOne), nullValue()); assertFalse(m.containsKey(keyOne)); assertTrue(m.isEmpty()); SpecialInteger keyThree = generator.generate(rndm.nextInt()); SpecialInteger valueFour = generator.generate(rndm.nextInt()); m.putIfAbsent(keyThree, valueFour); assertThat(m.size(), is(1)); assertFalse(m.remove(keyThree, null)); assertFalse(m.remove(keyThree, null)); assertFalse(m.remove(keyThree, keyThree)); assertTrue(m.remove(keyThree, valueFour)); assertTrue(m.isEmpty()); } @Test public final void testSubCollections() throws InterruptedException { final Map<SpecialInteger, SpecialInteger> m = createMap(generator); final Throwable[] failure = new Throwable[1]; final int maximumSize = 1000; Thread loader = new Thread() { @Override public void run() { for (int i = 0; i < maximumSize; i++) { m.put(generator.generate(i), generator.generate(i)); } } }; Thread tester = new Thread(new SubCollectionTester(m, maximumSize, failure)); tester.start(); loader.start(); loader.join(); tester.join(); if (failure[0] != null) { throw new AssertionError(failure[0]); } } @Test public final void testConcurrentSubCollections() throws InterruptedException { final Map<SpecialInteger, SpecialInteger> m = createMap(generator); final Throwable[] failure = new Throwable[1]; final int maximumSize = 1000; Thread loader = new Thread() { @Override public void run() { for (int i = 0; i < maximumSize; i++) { m.put(generator.generate(i), generator.generate(i)); } } }; Thread tester1 = new Thread(new SubCollectionTester(m, maximumSize, failure)); Thread tester2 = new Thread(new SubCollectionTester(m, maximumSize, failure)); tester1.start(); tester2.start(); loader.start(); loader.join(); tester1.join(); tester2.join(); if (failure[0] != null) { throw new AssertionError(failure[0]); } } @Test public final void testConcurrentRemoval() { ConcurrentMap<SpecialInteger, SpecialInteger> m = createMap(generator); assertTrue(m.isEmpty()); m.put(generator.generate(1), generator.generate(2)); Iterator<Map.Entry<SpecialInteger, SpecialInteger>> it = m.entrySet().iterator(); assertTrue(it.hasNext()); assertThat(m.remove(generator.generate(1)), is(generator.generate(2))); Map.Entry<SpecialInteger, SpecialInteger> e = it.next(); assertTrue(m.isEmpty()); assertThat(e.getKey(), is(generator.generate(1))); assertThat(e.getValue(), is(generator.generate(2))); } @Test public final void testConcurrentAddition() { ConcurrentMap<SpecialInteger, SpecialInteger> m = createMap(generator); m.put(generator.generate(1), generator.generate(2)); Iterator<Map.Entry<SpecialInteger, SpecialInteger>> it = m.entrySet().iterator(); Assert.assertTrue(it.hasNext()); Assert.assertEquals(generator.generate(2), m.put(generator.generate(1), generator.generate(3))); Map.Entry<SpecialInteger, SpecialInteger> e = it.next(); Assert.assertEquals(generator.generate(1), e.getKey()); Assert.assertThat(e.getValue(), anyOf(equalTo(generator.generate(2)), equalTo(generator.generate(3)))); } @Test public final void testConcurrentIterationAndMutation() { final ConcurrentMap<SpecialInteger, SpecialInteger> m = createMap(generator); final AtomicBoolean stopped = new AtomicBoolean(false); Thread mutator = new Thread() { @Override public void run() { int counter = 0; Random rndm = new Random(); while (!stopped.get()) { if (++counter == 25) { counter = 0; // Fixing reader starvation on some JVMs / Hardware combinations Thread.yield(); } int v = rndm.nextInt(8192); if (rndm.nextBoolean()) { m.remove(generator.generate(v)); } else { m.put(generator.generate(v), generator.generate(v)); } } } }; mutator.start(); boolean interrupted = false; try { for (int i = 0; i < 10; i++) { int checked = 0; for (Map.Entry<SpecialInteger, SpecialInteger> e : m.entrySet()) { Assert.assertEquals(e.getKey(), e.getValue()); checked++; } System.out.println("Finished Read Pass " + i + " : Checked " + checked); if (checked == 0) { try { Thread.sleep(100); } catch (InterruptedException e) { interrupted = true; } } else { Thread.yield(); } } } finally { stopped.set(true); while (mutator.isAlive()) { try { mutator.join(); } catch (InterruptedException e) { interrupted = true; } } if (interrupted) { Thread.currentThread().interrupt(); } } } @Test public final void testConcurrentIterationAndMutationWithHalfOffHeapStorageEngine() { final Map<Integer, byte[]> m = createOffHeapBufferMap(new UnlimitedPageSource(new HeapBufferSource())); final AtomicReference<Throwable> mutatorExceptionRef = new AtomicReference<Throwable>(); final AtomicBoolean stopped = new AtomicBoolean(false); Thread mutator = new Thread() { @Override public void run() { try { Random rndm = new Random(); while (!stopped.get()) { Integer v = rndm.nextInt(8192); if (rndm.nextBoolean()) { m.remove(v); } else { byte[] value = new byte[v >>> 3]; for (int i = 0; i < value.length; i++) { value[i] = (byte) (i % 10); } m.put(v, value); } } } catch (Throwable t) { mutatorExceptionRef.set(t); } } }; mutator.start(); boolean interrupted = false; try { for (int i = 0; i < 10; i++) { int checked = 0; for (Map.Entry<Integer, byte[]> e : m.entrySet()) { Integer key = e.getKey(); byte[] value = e.getValue(); Assert.assertEquals(key.intValue() >>> 3, value.length); for (int j = 0; j < value.length; j++) { Assert.assertEquals(j % 10, value[j]); } checked++; } if (checked == 0) { try { Thread.sleep(100); } catch (InterruptedException e) { interrupted = true; } } else { Thread.yield(); } } } finally { stopped.set(true); while (mutator.isAlive()) { try { mutator.join(); } catch (InterruptedException e) { interrupted = true; } } if (interrupted) { Thread.currentThread().interrupt(); } } Throwable mutatorException = mutatorExceptionRef.get(); if (mutatorException != null) { throw new AssertionError(mutatorException); } } @Test public final void testConcurrentIterationMutationAndClearingWithHalfOffHeapStorageEngine() { final Map<Integer, byte[]> m = createOffHeapBufferMap(new UnlimitedPageSource(new HeapBufferSource())); final AtomicReference<Throwable> mutatorExceptionRef = new AtomicReference<Throwable>(); final AtomicBoolean stopped = new AtomicBoolean(false); Thread mutator = new Thread() { @Override public void run() { try { Random rndm = new Random(); while (!stopped.get()) { Integer v = rndm.nextInt(8192); if (rndm.nextInt(100) == 0) { m.clear(); } else if (rndm.nextBoolean()) { m.remove(v); } else { byte[] value = new byte[v >>> 3]; for (int i = 0; i < value.length; i++) { value[i] = (byte) (i % 10); } m.put(v, value); } } } catch (Throwable t) { mutatorExceptionRef.set(t); } } }; mutator.start(); boolean interrupted = false; try { for (int i = 0; i < 10; i++) { int checked = 0; for (Map.Entry<Integer, byte[]> e : m.entrySet()) { Integer key = e.getKey(); byte[] value = e.getValue(); Assert.assertEquals(key.intValue() >>> 3, value.length); for (int j = 0; j < value.length; j++) { Assert.assertEquals(j % 10, value[j]); } checked++; } if (checked == 0) { try { Thread.sleep(100); } catch (InterruptedException e) { interrupted = true; } } else { Thread.yield(); } } } finally { stopped.set(true); while (mutator.isAlive()) { try { mutator.join(); } catch (InterruptedException e) { interrupted = true; } } if (interrupted) { Thread.currentThread().interrupt(); } } Throwable mutatorException = mutatorExceptionRef.get(); if (mutatorException != null) { throw new AssertionError(mutatorException); } } @Test public void testConcurrentUpdateOfKeyWhileIterating() { final Map<SpecialInteger, SpecialInteger> m = createMap(generator); assumeThat(m, not(either(instanceOf(AbstractConcurrentOffHeapCache.class)) .or(instanceOf(AbstractOffHeapClockCache.class)))); m.put(generator.generate(1024), generator.generate(1024)); m.put(generator.generate(1), generator.generate(1)); List<Iterator<SpecialInteger>> iterators = new ArrayList<Iterator<SpecialInteger>>(1024); for (int i = 0; i < 1024; i++) { iterators.add(m.keySet().iterator()); m.put(generator.generate(i), generator.generate(i)); m.put(generator.generate(1024), generator.generate(i)); } for (Iterator<SpecialInteger> it : iterators) { boolean foundMagicKey = false; while (it.hasNext()) { if (it.next().equals(generator.generate(1024))) { foundMagicKey = true; } } assertTrue(foundMagicKey); } } static class SubCollectionTester implements Runnable { private final Throwable[] failure; private final Map<?, ?> map; private final int maximumSize; private int previousSize = 0; SubCollectionTester(Map<?, ?> map, int max, Throwable[] failure) { this.failure = failure; this.map = map; this.maximumSize = max; } private boolean check(int size) { if (size < previousSize) throw new AssertionError("Map has shrunk?"); if (size > maximumSize) throw new AssertionError("Max bigger than possible?"); if (size == maximumSize) return true; previousSize = size; return false; } @Override public void run() { try { while (true) { if (check(map.keySet().size())) return; if (check(map.entrySet().size())) return; if (check(map.values().size())) return; if (check(map.keySet().toArray().length)) return; if (check(map.entrySet().toArray().length)) return; if (check(map.values().toArray().length)) return; } } catch (Throwable t) { failure[0] = t; } } } }