/*
* Copyright (C) 2012 Facebook, Inc.
*
* 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 com.facebook.concurrency;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static com.facebook.testing.TestUtils.waitUntilThreadBlocks;
public abstract class AbstractTestConcurrentCache {
private static final String STD_KEY = "std";
private static final String BLOCKING_KEY = "blocking";
private static final String EXCEPTION_KEY = "exception";
private static final String VALUE = "value";
private ConcurrentCacheTestHelper<String, String> testHelper;
private BlockingValueProducer<String, RuntimeException> stdProducer;
private BlockingValueProducer<String, RuntimeException> blockingProducer;
private BlockingValueProducer<String, RuntimeException> exceptionThrowingProducer;
private ConcurrentCache<String, String, RuntimeException> cache;
protected abstract ConcurrentCache<String, String, RuntimeException> createCache(
ValueFactory<String, String, RuntimeException> valueFactory
);
@BeforeMethod(alwaysRun = true)
public void setUp() throws Exception {
stdProducer = new BlockingValueProducer<>(VALUE);
blockingProducer = new BlockingValueProducer<>(VALUE, true);
exceptionThrowingProducer =
new BlockingValueProducer<>(
VALUE, true, new RuntimeException("I take exception to this")
);
cache = createCache(
input -> {
switch (input) {
case STD_KEY:
return stdProducer.call();
case BLOCKING_KEY:
return blockingProducer.call();
case EXCEPTION_KEY:
return exceptionThrowingProducer.call();
default:
return input;
}
}
);
testHelper = new ConcurrentCacheTestHelper<>(cache);
}
@Test
public void testProducerThrowsException() throws Exception {
// exception should be seen by all who try to do a get
Thread t1 = testHelper.getInThread(EXCEPTION_KEY, VALUE);
Thread t2 = testHelper.getInThread(EXCEPTION_KEY, VALUE);
exceptionThrowingProducer.signal();
t1.join();
t2.join();
try {
cache.get(EXCEPTION_KEY);
Assert.fail("expected exception");
} catch (RuntimeException e) {
Assert.assertEquals(
testHelper.getExceptionList().size(), 2, "all threads did not see exceptions"
);
Assert.assertTrue(cache.getIfPresent(EXCEPTION_KEY) != null, "task not inserted");
}
}
/**
* case that while a value is being produced for a key, a removal happens.
* Since remove blocks on
*/
@Test
public void testConcurrentGetAndRemove() throws Exception {
Thread getThread = testHelper.getInThread(BLOCKING_KEY, VALUE);
// wait until the task is inserted
waitUntilThreadBlocks(getThread);
// now start the remove
Thread removeThread = testHelper.removeInThread(BLOCKING_KEY, VALUE);
waitUntilThreadBlocks(removeThread);
// let both proceed
blockingProducer.signal();
getThread.join();
removeThread.join();
Assert.assertEquals(blockingProducer.getCalledCount(), 1);
Assert.assertFalse(cache.getIfPresent(VALUE) != null, "key should not exist");
}
// @Test
public void testConcurrentGetAndClear() throws Exception {
// TOOD: see if we can fix this test; we need a way to block
// *after* we do the value insert; or we can do various tryAcquire on
// locks...
Thread getThread = testHelper.getInThread(BLOCKING_KEY, VALUE);
// wait until the task is inserted, but no value is produced
waitUntilThreadBlocks(getThread);
// now clear
Thread clearThread = testHelper.clearInThread();
clearThread.join();
blockingProducer.signal();
// let get proceed
getThread.join();
getThread.join();
Assert.assertEquals(blockingProducer.getCalledCount(), 1);
Assert.assertFalse(cache.getIfPresent(BLOCKING_KEY) != null, "key should not exist");
}
@Test
public void testIterator() throws Exception {
// sanity check that our iterator works as expected
cache.get("fuu");
cache.get("bar");
cache.get("baz");
cache.get("wombat");
int i = 0;
for (Map.Entry<String, CallableSnapshot<String, RuntimeException>> entry :
cache) {
// the value producer used returns the key as the value
Assert.assertEquals(entry.getKey(), entry.getValue().get());
i++;
}
Assert.assertEquals(i, 4);
}
@Test
public void testCacheFlow() throws Exception {
// not called yet
Assert.assertEquals(stdProducer.getCalledCount(), 0);
// we should get the expected value on a cache-miss
Assert.assertEquals(cache.get(STD_KEY), VALUE);
// and see 1 call to the factory
Assert.assertEquals(stdProducer.getCalledCount(), 1);
// now we have a cache hit
Assert.assertEquals(cache.get(STD_KEY), VALUE);
// that does not invoke the factory again
Assert.assertEquals(stdProducer.getCalledCount(), 1);
// now remove the cache entry
Assert.assertEquals(cache.remove(STD_KEY), VALUE);
// produce again
Assert.assertEquals(cache.get(STD_KEY), VALUE);
// results in 2nd call
Assert.assertEquals(stdProducer.getCalledCount(), 2);
}
/**
* produces two threads doing get() at the same time. Waits for both
* to block and then asserts we only saw one factory.call()
*/
@Test
public void testConcurrentCacheHit() throws Throwable {
// run each get in a separate thread
Thread t1 = testHelper.getInThread(STD_KEY, VALUE);
Thread t2 = testHelper.getInThread(STD_KEY, VALUE);
// wait for both threads to block or terminate
waitUntilThreadBlocks(t1);
waitUntilThreadBlocks(t2);
// signal the value factory to let one continue
stdProducer.signal();
//wait for threads
t1.join();
t2.join();
// did either thread throw an exception?
if (!testHelper.getExceptionList().isEmpty()) {
throw testHelper.getExceptionList().get(0);
}
// only 1 call
Assert.assertEquals(stdProducer.getCalledCount(), 1);
}
@Test
public void testRemoveIfError() throws Exception {
// allow the cache.get() to proceed below
exceptionThrowingProducer.signal();
try {
cache.get(EXCEPTION_KEY);
Assert.fail("expected exception");
} catch (RuntimeException e) {
// expected
}
// now call removeIfError() twice, with the first one
final AtomicInteger removeCount = new AtomicInteger(0);
Runnable operation = () -> {
if (cache.removeIfError(EXCEPTION_KEY)) {
removeCount.incrementAndGet();
}
};
// this really tests that get(error), removeIfError(), get(success),
// removeIfError() won't remove the successful get
Thread t1 = testHelper.doInThread(operation);
Thread t2 = testHelper.getInThread(BLOCKING_KEY, VALUE);
waitUntilThreadBlocks(t1);
waitUntilThreadBlocks(t2);
blockingProducer.signal();
// now there is a valid value, and this should not remove it
Thread t3 = testHelper.doInThread(operation);
waitUntilThreadBlocks(t3);
// should see only one remove
Assert.assertEquals(removeCount.get(), 1);
}
@Test
public void testRemoveBeforeValueSwap() throws Exception {
// Initiate a value fetch
Thread t1 = testHelper.getInThread(BLOCKING_KEY, VALUE);
waitUntilThreadBlocks(t1);
// Remove the cached value producer before it is swapped with its value
cache.clear();
// Now allow the original thread to finish producing the value and to do the
// value swap
blockingProducer.signal();
t1.join();
// There should not be a value present
Assert.assertNull(cache.getIfPresent(BLOCKING_KEY));
Assert.assertEquals(blockingProducer.getCalledCount(), 1);
}
}