/* * Copyright (C) 2011 The Guava Authors * * 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.google.common.cache; import static com.google.common.cache.TestingCacheLoaders.bulkLoader; import static com.google.common.cache.TestingCacheLoaders.constantLoader; import static com.google.common.cache.TestingCacheLoaders.errorLoader; import static com.google.common.cache.TestingCacheLoaders.exceptionLoader; import static com.google.common.cache.TestingCacheLoaders.identityLoader; import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener; import static com.google.common.truth.Truth.assertThat; import static java.lang.Thread.currentThread; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.google.common.cache.CacheLoader.InvalidCacheLoadException; import com.google.common.cache.TestingCacheLoaders.CountingLoader; import com.google.common.cache.TestingCacheLoaders.IdentityLoader; import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.testing.FakeTicker; import com.google.common.testing.TestLogHandler; import com.google.common.util.concurrent.Callables; import com.google.common.util.concurrent.ExecutionError; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.UncheckedExecutionException; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.logging.LogRecord; import junit.framework.TestCase; /** * Tests relating to cache loading: concurrent loading, exceptions during loading, etc. * * @author mike nonemacher */ public class CacheLoadingTest extends TestCase { TestLogHandler logHandler; @Override public void setUp() throws Exception { super.setUp(); logHandler = new TestLogHandler(); LocalCache.logger.addHandler(logHandler); } @Override public void tearDown() throws Exception { super.tearDown(); // TODO(cpovirk): run tests in other thread instead of messing with main thread interrupt status currentThread().interrupted(); LocalCache.logger.removeHandler(logHandler); } private Throwable popLoggedThrowable() { List logRecords = logHandler.getStoredLogRecords(); assertEquals(1, logRecords.size()); LogRecord logRecord = logRecords.get(0); logHandler.clear(); return logRecord.getThrown(); } private void checkNothingLogged() { assertThat(logHandler.getStoredLogRecords()).isEmpty(); } private void checkLoggedCause(Throwable t) { assertThat(popLoggedThrowable()).hasCauseThat().isSameInstanceAs(t); } private void checkLoggedInvalidLoad() { assertThat(popLoggedThrowable()).isInstanceOf(InvalidCacheLoadException.class); } public void testLoad() throws ExecutionException { LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(identityLoader()); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); Object key = new Object(); assertSame(key, cache.get(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); key = new Object(); assertSame(key, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); key = new Object(); cache.refresh(key); checkNothingLogged(); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(3, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(key, cache.get(key)); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(3, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); Object value = new Object(); // callable is not called assertSame(key, cache.get(key, throwing(new Exception()))); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(3, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); key = new Object(); assertSame(value, cache.get(key, Callables.returning(value))); stats = cache.stats(); assertEquals(3, stats.missCount()); assertEquals(4, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); } public void testReload() throws ExecutionException { final Object one = new Object(); final Object two = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFuture(two); } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkNothingLogged(); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(two, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testRefresh() { final Object one = new Object(); final Object two = new Object(); FakeTicker ticker = new FakeTicker(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFuture(two); } }; LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .ticker(ticker) .refreshAfterWrite(1, MILLISECONDS) .build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(two, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(two, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); } public void testRefresh_getIfPresent() { final Object one = new Object(); final Object two = new Object(); FakeTicker ticker = new FakeTicker(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFuture(two); } }; LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .ticker(ticker) .refreshAfterWrite(1, MILLISECONDS) .build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getIfPresent(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(two, cache.getIfPresent(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(two, cache.getIfPresent(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); } public void testBulkLoad_default() throws ExecutionException { LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .build(TestingCacheLoaders.identityLoader()); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertEquals(ImmutableMap.of(), cache.getAll(ImmutableList.of())); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertEquals(ImmutableMap.of(1, 1), cache.getAll(asList(1))); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertEquals(ImmutableMap.of(1, 1, 2, 2, 3, 3, 4, 4), cache.getAll(asList(1, 2, 3, 4))); stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(4, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); assertEquals(ImmutableMap.of(2, 2, 3, 3), cache.getAll(asList(2, 3))); stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(4, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); // duplicate keys are ignored, and don't impact stats assertEquals(ImmutableMap.of(4, 4, 5, 5), cache.getAll(asList(4, 5))); stats = cache.stats(); assertEquals(5, stats.missCount()); assertEquals(5, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(4, stats.hitCount()); } public void testBulkLoad_loadAll() throws ExecutionException { IdentityLoader backingLoader = identityLoader(); CacheLoader loader = bulkLoader(backingLoader); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertEquals(ImmutableMap.of(), cache.getAll(ImmutableList.of())); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertEquals(ImmutableMap.of(1, 1), cache.getAll(asList(1))); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertEquals(ImmutableMap.of(1, 1, 2, 2, 3, 3, 4, 4), cache.getAll(asList(1, 2, 3, 4))); stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); assertEquals(ImmutableMap.of(2, 2, 3, 3), cache.getAll(asList(2, 3))); stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(2, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); // duplicate keys are ignored, and don't impact stats assertEquals(ImmutableMap.of(4, 4, 5, 5), cache.getAll(asList(4, 5))); stats = cache.stats(); assertEquals(5, stats.missCount()); assertEquals(3, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(4, stats.hitCount()); } public void testBulkLoad_extra() throws ExecutionException { CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) throws Exception { return new Object(); } @Override public Map loadAll(Iterable keys) throws Exception { Map result = Maps.newHashMap(); for (Object key : keys) { Object value = new Object(); result.put(key, value); // add extra entries result.put(value, key); } return result; } }; LoadingCache cache = CacheBuilder.newBuilder().build(loader); Object[] lookupKeys = new Object[] {new Object(), new Object(), new Object()}; Map result = cache.getAll(asList(lookupKeys)); assertThat(result.keySet()).containsExactlyElementsIn(asList(lookupKeys)); for (Entry entry : result.entrySet()) { Object key = entry.getKey(); Object value = entry.getValue(); assertSame(value, result.get(key)); assertNull(result.get(value)); assertSame(value, cache.asMap().get(key)); assertSame(key, cache.asMap().get(value)); } } public void testBulkLoad_clobber() throws ExecutionException { final Object extraKey = new Object(); final Object extraValue = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) throws Exception { throw new AssertionError(); } @Override public Map loadAll(Iterable keys) throws Exception { Map result = Maps.newHashMap(); for (Object key : keys) { Object value = new Object(); result.put(key, value); } result.put(extraKey, extraValue); return result; } }; LoadingCache cache = CacheBuilder.newBuilder().build(loader); cache.asMap().put(extraKey, extraKey); assertSame(extraKey, cache.asMap().get(extraKey)); Object[] lookupKeys = new Object[] {new Object(), new Object(), new Object()}; Map result = cache.getAll(asList(lookupKeys)); assertThat(result.keySet()).containsExactlyElementsIn(asList(lookupKeys)); for (Entry entry : result.entrySet()) { Object key = entry.getKey(); Object value = entry.getValue(); assertSame(value, result.get(key)); assertSame(value, cache.asMap().get(key)); } assertNull(result.get(extraKey)); assertSame(extraValue, cache.asMap().get(extraKey)); } public void testBulkLoad_clobberNullValue() throws ExecutionException { final Object extraKey = new Object(); final Object extraValue = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) throws Exception { throw new AssertionError(); } @Override public Map loadAll(Iterable keys) throws Exception { Map result = Maps.newHashMap(); for (Object key : keys) { Object value = new Object(); result.put(key, value); } result.put(extraKey, extraValue); result.put(extraValue, null); return result; } }; LoadingCache cache = CacheBuilder.newBuilder().build(loader); cache.asMap().put(extraKey, extraKey); assertSame(extraKey, cache.asMap().get(extraKey)); Object[] lookupKeys = new Object[] {new Object(), new Object(), new Object()}; try { cache.getAll(asList(lookupKeys)); fail(); } catch (InvalidCacheLoadException expected) { } for (Object key : lookupKeys) { assertTrue(cache.asMap().containsKey(key)); } assertSame(extraValue, cache.asMap().get(extraKey)); assertFalse(cache.asMap().containsKey(extraValue)); } public void testBulkLoad_clobberNullKey() throws ExecutionException { final Object extraKey = new Object(); final Object extraValue = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) throws Exception { throw new AssertionError(); } @Override public Map loadAll(Iterable keys) throws Exception { Map result = Maps.newHashMap(); for (Object key : keys) { Object value = new Object(); result.put(key, value); } result.put(extraKey, extraValue); result.put(null, extraKey); return result; } }; LoadingCache cache = CacheBuilder.newBuilder().build(loader); cache.asMap().put(extraKey, extraKey); assertSame(extraKey, cache.asMap().get(extraKey)); Object[] lookupKeys = new Object[] {new Object(), new Object(), new Object()}; try { cache.getAll(asList(lookupKeys)); fail(); } catch (InvalidCacheLoadException expected) { } for (Object key : lookupKeys) { assertTrue(cache.asMap().containsKey(key)); } assertSame(extraValue, cache.asMap().get(extraKey)); assertFalse(cache.asMap().containsValue(extraKey)); } public void testBulkLoad_partial() throws ExecutionException { final Object extraKey = new Object(); final Object extraValue = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) throws Exception { throw new AssertionError(); } @Override public Map loadAll(Iterable keys) throws Exception { Map result = Maps.newHashMap(); // ignore request keys result.put(extraKey, extraValue); return result; } }; LoadingCache cache = CacheBuilder.newBuilder().build(loader); Object[] lookupKeys = new Object[] {new Object(), new Object(), new Object()}; try { cache.getAll(asList(lookupKeys)); fail(); } catch (InvalidCacheLoadException expected) { } assertSame(extraValue, cache.asMap().get(extraKey)); } public void testLoadNull() throws ExecutionException { LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(constantLoader(null)); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.get(new Object()); fail(); } catch (InvalidCacheLoadException expected) { } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getUnchecked(new Object()); fail(); } catch (InvalidCacheLoadException expected) { } stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(new Object()); checkLoggedInvalidLoad(); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(3, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.get(new Object(), Callables.returning(null)); fail(); } catch (InvalidCacheLoadException expected) { } stats = cache.stats(); assertEquals(3, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(4, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (InvalidCacheLoadException expected) { } stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(5, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testReloadNull() throws ExecutionException { final Object one = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return null; } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedInvalidLoad(); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testReloadNullFuture() throws ExecutionException { final Object one = new Object(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFuture(null); } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedInvalidLoad(); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testRefreshNull() { final Object one = new Object(); FakeTicker ticker = new FakeTicker(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFuture(null); } }; LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .ticker(ticker) .refreshAfterWrite(1, MILLISECONDS) .build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); // refreshed stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); } public void testBulkLoadNull() throws ExecutionException { LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(bulkLoader(constantLoader(null))); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (InvalidCacheLoadException expected) { } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testBulkLoadNullMap() throws ExecutionException { LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .build( new CacheLoader() { @Override public Object load(Object key) { throw new AssertionError(); } @Override public Map loadAll(Iterable keys) { return null; } }); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (InvalidCacheLoadException expected) { } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testLoadError() throws ExecutionException { Error e = new Error(); CacheLoader loader = errorLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.get(new Object()); fail(); } catch (ExecutionError expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getUnchecked(new Object()); fail(); } catch (ExecutionError expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(new Object()); checkLoggedCause(e); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(3, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); final Error callableError = new Error(); try { cache.get( new Object(), new Callable() { @Override public Object call() { throw callableError; } }); fail(); } catch (ExecutionError expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(callableError); } stats = cache.stats(); assertEquals(3, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(4, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (ExecutionError expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(5, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testReloadError() throws ExecutionException { final Object one = new Object(); final Error e = new Error(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { throw e; } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedCause(e); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testReloadFutureError() throws ExecutionException { final Object one = new Object(); final Error e = new Error(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFailedFuture(e); } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedCause(e); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testRefreshError() { final Object one = new Object(); final Error e = new Error(); FakeTicker ticker = new FakeTicker(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFailedFuture(e); } }; LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .ticker(ticker) .refreshAfterWrite(1, MILLISECONDS) .build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); // refreshed stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); } public void testBulkLoadError() throws ExecutionException { Error e = new Error(); CacheLoader loader = errorLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(bulkLoader(loader)); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (ExecutionError expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testLoadCheckedException() { Exception e = new Exception(); CacheLoader loader = exceptionLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.get(new Object()); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getUnchecked(new Object()); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(new Object()); checkLoggedCause(e); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(3, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); Exception callableException = new Exception(); try { cache.get(new Object(), throwing(callableException)); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(callableException); } stats = cache.stats(); assertEquals(3, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(4, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(5, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testLoadInterruptedException() { Exception e = new InterruptedException(); CacheLoader loader = exceptionLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); // Sanity check: assertFalse(currentThread().interrupted()); try { cache.get(new Object()); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } assertTrue(currentThread().interrupted()); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getUnchecked(new Object()); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } assertTrue(currentThread().interrupted()); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(new Object()); assertTrue(currentThread().interrupted()); checkLoggedCause(e); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(3, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); Exception callableException = new InterruptedException(); try { cache.get(new Object(), throwing(callableException)); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(callableException); } assertTrue(currentThread().interrupted()); stats = cache.stats(); assertEquals(3, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(4, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } assertTrue(currentThread().interrupted()); stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(5, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testReloadCheckedException() { final Object one = new Object(); final Exception e = new Exception(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) throws Exception { throw e; } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedCause(e); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testReloadFutureCheckedException() { final Object one = new Object(); final Exception e = new Exception(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFailedFuture(e); } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedCause(e); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testRefreshCheckedException() { final Object one = new Object(); final Exception e = new Exception(); FakeTicker ticker = new FakeTicker(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFailedFuture(e); } }; LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .ticker(ticker) .refreshAfterWrite(1, MILLISECONDS) .build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); // refreshed stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); } public void testBulkLoadCheckedException() { Exception e = new Exception(); CacheLoader loader = exceptionLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(bulkLoader(loader)); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testBulkLoadInterruptedException() { Exception e = new InterruptedException(); CacheLoader loader = exceptionLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(bulkLoader(loader)); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (ExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } assertTrue(currentThread().interrupted()); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testLoadUncheckedException() throws ExecutionException { Exception e = new RuntimeException(); CacheLoader loader = exceptionLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.get(new Object()); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getUnchecked(new Object()); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(new Object()); checkLoggedCause(e); stats = cache.stats(); assertEquals(2, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(3, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); Exception callableException = new RuntimeException(); try { cache.get(new Object(), throwing(callableException)); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(callableException); } stats = cache.stats(); assertEquals(3, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(4, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(4, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(5, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testReloadUncheckedException() throws ExecutionException { final Object one = new Object(); final Exception e = new RuntimeException(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) throws Exception { throw e; } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedCause(e); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testReloadFutureUncheckedException() throws ExecutionException { final Object one = new Object(); final Exception e = new RuntimeException(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFailedFuture(e); } }; LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); cache.refresh(key); checkLoggedCause(e); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); } public void testRefreshUncheckedException() { final Object one = new Object(); final Exception e = new RuntimeException(); FakeTicker ticker = new FakeTicker(); CacheLoader loader = new CacheLoader() { @Override public Object load(Object key) { return one; } @Override public ListenableFuture reload(Object key, Object oldValue) { return Futures.immediateFailedFuture(e); } }; LoadingCache cache = CacheBuilder.newBuilder() .recordStats() .ticker(ticker) .refreshAfterWrite(1, MILLISECONDS) .build(loader); Object key = new Object(); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(1, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); // refreshed stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(2, stats.hitCount()); ticker.advance(1, MILLISECONDS); assertSame(one, cache.getUnchecked(key)); stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(1, stats.loadSuccessCount()); assertEquals(2, stats.loadExceptionCount()); assertEquals(3, stats.hitCount()); } public void testBulkLoadUncheckedException() throws ExecutionException { Exception e = new RuntimeException(); CacheLoader loader = exceptionLoader(e); LoadingCache cache = CacheBuilder.newBuilder().recordStats().build(bulkLoader(loader)); CacheStats stats = cache.stats(); assertEquals(0, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(0, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); try { cache.getAll(asList(new Object())); fail(); } catch (UncheckedExecutionException expected) { assertThat(expected).hasCauseThat().isSameInstanceAs(e); } stats = cache.stats(); assertEquals(1, stats.missCount()); assertEquals(0, stats.loadSuccessCount()); assertEquals(1, stats.loadExceptionCount()); assertEquals(0, stats.hitCount()); } public void testReloadAfterFailure() throws ExecutionException { final AtomicInteger count = new AtomicInteger(); final Exception e = new IllegalStateException("exception to trigger failure on first load()"); CacheLoader failOnceFunction = new CacheLoader() { @Override public String load(Integer key) throws Exception { if (count.getAndIncrement() == 0) { throw e; } return key.toString(); } }; CountingRemovalListener removalListener = countingRemovalListener(); LoadingCache cache = CacheBuilder.newBuilder().removalListener(removalListener).build(failOnceFunction); try { cache.getUnchecked(1); fail(); } catch (UncheckedExecutionException ue) { assertThat(ue).hasCauseThat().isSameInstanceAs(e); } assertEquals("1", cache.getUnchecked(1)); assertEquals(0, removalListener.getCount()); count.set(0); cache.refresh(2); checkLoggedCause(e); assertEquals("2", cache.getUnchecked(2)); assertEquals(0, removalListener.getCount()); } public void testReloadAfterValueReclamation() throws InterruptedException, ExecutionException { CountingLoader countingLoader = new CountingLoader(); LoadingCache cache = CacheBuilder.newBuilder().weakValues().build(countingLoader); ConcurrentMap map = cache.asMap(); int iterations = 10; WeakReference ref = new WeakReference<>(null); int expectedComputations = 0; for (int i = 0; i < iterations; i++) { // The entry should get garbage collected and recomputed. Object oldValue = ref.get(); if (oldValue == null) { expectedComputations++; } ref = new WeakReference<>(cache.getUnchecked(1)); oldValue = null; Thread.sleep(i); System.gc(); } assertEquals(expectedComputations, countingLoader.getCount()); for (int i = 0; i < iterations; i++) { // The entry should get garbage collected and recomputed. Object oldValue = ref.get(); if (oldValue == null) { expectedComputations++; } cache.refresh(1); checkNothingLogged(); ref = new WeakReference<>(map.get(1)); oldValue = null; Thread.sleep(i); System.gc(); } assertEquals(expectedComputations, countingLoader.getCount()); } public void testReloadAfterSimulatedValueReclamation() throws ExecutionException { CountingLoader countingLoader = new CountingLoader(); LoadingCache cache = CacheBuilder.newBuilder().concurrencyLevel(1).weakValues().build(countingLoader); Object key = new Object(); assertNotNull(cache.getUnchecked(key)); CacheTesting.simulateValueReclamation(cache, key); // this blocks if computation can't deal with partially-collected values assertNotNull(cache.getUnchecked(key)); assertEquals(1, cache.size()); assertEquals(2, countingLoader.getCount()); CacheTesting.simulateValueReclamation(cache, key); cache.refresh(key); checkNothingLogged(); assertEquals(1, cache.size()); assertEquals(3, countingLoader.getCount()); } public void testReloadAfterSimulatedKeyReclamation() throws ExecutionException { CountingLoader countingLoader = new CountingLoader(); LoadingCache cache = CacheBuilder.newBuilder().concurrencyLevel(1).weakKeys().build(countingLoader); Object key = new Object(); assertNotNull(cache.getUnchecked(key)); assertEquals(1, cache.size()); CacheTesting.simulateKeyReclamation(cache, key); // this blocks if computation can't deal with partially-collected values assertNotNull(cache.getUnchecked(key)); assertEquals(2, countingLoader.getCount()); CacheTesting.simulateKeyReclamation(cache, key); cache.refresh(key); checkNothingLogged(); assertEquals(3, countingLoader.getCount()); } /** * Make sure LoadingCache correctly wraps ExecutionExceptions and UncheckedExecutionExceptions. */ public void testLoadingExceptionWithCause() { final Exception cause = new Exception(); final UncheckedExecutionException uee = new UncheckedExecutionException(cause); final ExecutionException ee = new ExecutionException(cause); LoadingCache cacheUnchecked = CacheBuilder.newBuilder().build(exceptionLoader(uee)); LoadingCache cacheChecked = CacheBuilder.newBuilder().build(exceptionLoader(ee)); try { cacheUnchecked.get(new Object()); fail(); } catch (ExecutionException e) { fail(); } catch (UncheckedExecutionException caughtEe) { assertThat(caughtEe).hasCauseThat().isSameInstanceAs(uee); } try { cacheUnchecked.getUnchecked(new Object()); fail(); } catch (UncheckedExecutionException caughtUee) { assertThat(caughtUee).hasCauseThat().isSameInstanceAs(uee); } cacheUnchecked.refresh(new Object()); checkLoggedCause(uee); try { cacheUnchecked.getAll(asList(new Object())); fail(); } catch (ExecutionException e) { fail(); } catch (UncheckedExecutionException caughtEe) { assertThat(caughtEe).hasCauseThat().isSameInstanceAs(uee); } try { cacheChecked.get(new Object()); fail(); } catch (ExecutionException caughtEe) { assertThat(caughtEe).hasCauseThat().isSameInstanceAs(ee); } try { cacheChecked.getUnchecked(new Object()); fail(); } catch (UncheckedExecutionException caughtUee) { assertThat(caughtUee).hasCauseThat().isSameInstanceAs(ee); } cacheChecked.refresh(new Object()); checkLoggedCause(ee); try { cacheChecked.getAll(asList(new Object())); fail(); } catch (ExecutionException caughtEe) { assertThat(caughtEe).hasCauseThat().isSameInstanceAs(ee); } } public void testBulkLoadingExceptionWithCause() { final Exception cause = new Exception(); final UncheckedExecutionException uee = new UncheckedExecutionException(cause); final ExecutionException ee = new ExecutionException(cause); LoadingCache cacheUnchecked = CacheBuilder.newBuilder().build(bulkLoader(exceptionLoader(uee))); LoadingCache cacheChecked = CacheBuilder.newBuilder().build(bulkLoader(exceptionLoader(ee))); try { cacheUnchecked.getAll(asList(new Object())); fail(); } catch (ExecutionException e) { fail(); } catch (UncheckedExecutionException caughtEe) { assertThat(caughtEe).hasCauseThat().isSameInstanceAs(uee); } try { cacheChecked.getAll(asList(new Object())); fail(); } catch (ExecutionException caughtEe) { assertThat(caughtEe).hasCauseThat().isSameInstanceAs(ee); } } public void testConcurrentLoading() throws InterruptedException { testConcurrentLoading(CacheBuilder.newBuilder()); } private static void testConcurrentLoading(CacheBuilder builder) throws InterruptedException { testConcurrentLoadingDefault(builder); testConcurrentLoadingNull(builder); testConcurrentLoadingUncheckedException(builder); testConcurrentLoadingCheckedException(builder); } public void testConcurrentExpirationLoading() throws InterruptedException { testConcurrentLoading(CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS)); } /** * On a successful concurrent computation, only one thread does the work, but all the threads get * the same result. */ private static void testConcurrentLoadingDefault(CacheBuilder builder) throws InterruptedException { int count = 10; final AtomicInteger callCount = new AtomicInteger(); final CountDownLatch startSignal = new CountDownLatch(count + 1); final Object result = new Object(); LoadingCache cache = builder.build( new CacheLoader() { @Override public Object load(String key) throws InterruptedException { callCount.incrementAndGet(); startSignal.await(); return result; } }); List resultArray = doConcurrentGet(cache, "bar", count, startSignal); assertEquals(1, callCount.get()); for (int i = 0; i < count; i++) { assertSame("result(" + i + ") didn't match expected", result, resultArray.get(i)); } } /** * On a concurrent computation that returns null, all threads should get an * InvalidCacheLoadException, with the loader only called once. The result should not be cached (a * later request should call the loader again). */ private static void testConcurrentLoadingNull(CacheBuilder builder) throws InterruptedException { int count = 10; final AtomicInteger callCount = new AtomicInteger(); final CountDownLatch startSignal = new CountDownLatch(count + 1); LoadingCache cache = builder.build( new CacheLoader() { @Override public String load(String key) throws InterruptedException { callCount.incrementAndGet(); startSignal.await(); return null; } }); List result = doConcurrentGet(cache, "bar", count, startSignal); assertEquals(1, callCount.get()); for (int i = 0; i < count; i++) { assertThat(result.get(i)).isInstanceOf(InvalidCacheLoadException.class); } // subsequent calls should call the loader again, not get the old exception try { cache.getUnchecked("bar"); fail(); } catch (InvalidCacheLoadException expected) { } assertEquals(2, callCount.get()); } /** * On a concurrent computation that throws an unchecked exception, all threads should get the * (wrapped) exception, with the loader called only once. The result should not be cached (a later * request should call the loader again). */ private static void testConcurrentLoadingUncheckedException(CacheBuilder builder) throws InterruptedException { int count = 10; final AtomicInteger callCount = new AtomicInteger(); final CountDownLatch startSignal = new CountDownLatch(count + 1); final RuntimeException e = new RuntimeException(); LoadingCache cache = builder.build( new CacheLoader() { @Override public String load(String key) throws InterruptedException { callCount.incrementAndGet(); startSignal.await(); throw e; } }); List result = doConcurrentGet(cache, "bar", count, startSignal); assertEquals(1, callCount.get()); for (int i = 0; i < count; i++) { // doConcurrentGet alternates between calling getUnchecked and calling get, but an unchecked // exception thrown by the loader is always wrapped as an UncheckedExecutionException. assertThat(result.get(i)).isInstanceOf(UncheckedExecutionException.class); assertThat(((UncheckedExecutionException) result.get(i))).hasCauseThat().isSameInstanceAs(e); } // subsequent calls should call the loader again, not get the old exception try { cache.getUnchecked("bar"); fail(); } catch (UncheckedExecutionException expected) { } assertEquals(2, callCount.get()); } /** * On a concurrent computation that throws a checked exception, all threads should get the * (wrapped) exception, with the loader called only once. The result should not be cached (a later * request should call the loader again). */ private static void testConcurrentLoadingCheckedException(CacheBuilder builder) throws InterruptedException { int count = 10; final AtomicInteger callCount = new AtomicInteger(); final CountDownLatch startSignal = new CountDownLatch(count + 1); final IOException e = new IOException(); LoadingCache cache = builder.build( new CacheLoader() { @Override public String load(String key) throws IOException, InterruptedException { callCount.incrementAndGet(); startSignal.await(); throw e; } }); List result = doConcurrentGet(cache, "bar", count, startSignal); assertEquals(1, callCount.get()); for (int i = 0; i < count; i++) { // doConcurrentGet alternates between calling getUnchecked and calling get. If we call get(), // we should get an ExecutionException; if we call getUnchecked(), we should get an // UncheckedExecutionException. int mod = i % 3; if (mod == 0 || mod == 2) { assertThat(result.get(i)).isInstanceOf(ExecutionException.class); assertThat((ExecutionException) result.get(i)).hasCauseThat().isSameInstanceAs(e); } else { assertThat(result.get(i)).isInstanceOf(UncheckedExecutionException.class); assertThat((UncheckedExecutionException) result.get(i)).hasCauseThat().isSameInstanceAs(e); } } // subsequent calls should call the loader again, not get the old exception try { cache.getUnchecked("bar"); fail(); } catch (UncheckedExecutionException expected) { } assertEquals(2, callCount.get()); } /** * Test-helper method that performs {@code nThreads} concurrent calls to {@code cache.get(key)} or * {@code cache.getUnchecked(key)}, and returns a List containing each of the results. The result * for any given call to {@code cache.get} or {@code cache.getUnchecked} is the value returned, or * the exception thrown. * *

As we iterate from {@code 0} to {@code nThreads}, threads with an even index will call * {@code getUnchecked}, and threads with an odd index will call {@code get}. If the cache throws * exceptions, this difference may be visible in the returned List. */ private static List doConcurrentGet( final LoadingCache cache, final K key, int nThreads, final CountDownLatch gettersStartedSignal) throws InterruptedException { final AtomicReferenceArray result = new AtomicReferenceArray<>(nThreads); final CountDownLatch gettersComplete = new CountDownLatch(nThreads); for (int i = 0; i < nThreads; i++) { final int index = i; Thread thread = new Thread( new Runnable() { @Override public void run() { gettersStartedSignal.countDown(); Object value = null; try { int mod = index % 3; if (mod == 0) { value = cache.get(key); } else if (mod == 1) { value = cache.getUnchecked(key); } else { cache.refresh(key); value = cache.get(key); } result.set(index, value); } catch (Throwable t) { result.set(index, t); } gettersComplete.countDown(); } }); thread.start(); // we want to wait until each thread is WAITING - one thread waiting inside CacheLoader.load // (in startSignal.await()), and the others waiting for that thread's result. while (thread.isAlive() && thread.getState() != Thread.State.WAITING) { Thread.yield(); } } gettersStartedSignal.countDown(); gettersComplete.await(); List resultList = Lists.newArrayListWithExpectedSize(nThreads); for (int i = 0; i < nThreads; i++) { resultList.add(result.get(i)); } return resultList; } public void testAsMapDuringLoading() throws InterruptedException, ExecutionException { final CountDownLatch getStartedSignal = new CountDownLatch(2); final CountDownLatch letGetFinishSignal = new CountDownLatch(1); final CountDownLatch getFinishedSignal = new CountDownLatch(2); final String getKey = "get"; final String refreshKey = "refresh"; final String suffix = "Suffix"; CacheLoader computeFunction = new CacheLoader() { @Override public String load(String key) throws InterruptedException { getStartedSignal.countDown(); letGetFinishSignal.await(); return key + suffix; } }; final LoadingCache cache = CacheBuilder.newBuilder().build(computeFunction); ConcurrentMap map = cache.asMap(); map.put(refreshKey, refreshKey); assertEquals(1, map.size()); assertFalse(map.containsKey(getKey)); assertSame(refreshKey, map.get(refreshKey)); new Thread() { @Override public void run() { cache.getUnchecked(getKey); getFinishedSignal.countDown(); } }.start(); new Thread() { @Override public void run() { cache.refresh(refreshKey); getFinishedSignal.countDown(); } }.start(); getStartedSignal.await(); // computation is in progress; asMap shouldn't have changed assertEquals(1, map.size()); assertFalse(map.containsKey(getKey)); assertSame(refreshKey, map.get(refreshKey)); // let computation complete letGetFinishSignal.countDown(); getFinishedSignal.await(); checkNothingLogged(); // asMap view should have been updated assertEquals(2, cache.size()); assertEquals(getKey + suffix, map.get(getKey)); assertEquals(refreshKey + suffix, map.get(refreshKey)); } public void testInvalidateDuringLoading() throws InterruptedException, ExecutionException { // computation starts; invalidate() is called on the key being computed, computation finishes final CountDownLatch computationStarted = new CountDownLatch(2); final CountDownLatch letGetFinishSignal = new CountDownLatch(1); final CountDownLatch getFinishedSignal = new CountDownLatch(2); final String getKey = "get"; final String refreshKey = "refresh"; final String suffix = "Suffix"; CacheLoader computeFunction = new CacheLoader() { @Override public String load(String key) throws InterruptedException { computationStarted.countDown(); letGetFinishSignal.await(); return key + suffix; } }; final LoadingCache cache = CacheBuilder.newBuilder().build(computeFunction); ConcurrentMap map = cache.asMap(); map.put(refreshKey, refreshKey); new Thread() { @Override public void run() { cache.getUnchecked(getKey); getFinishedSignal.countDown(); } }.start(); new Thread() { @Override public void run() { cache.refresh(refreshKey); getFinishedSignal.countDown(); } }.start(); computationStarted.await(); cache.invalidate(getKey); cache.invalidate(refreshKey); assertFalse(map.containsKey(getKey)); assertFalse(map.containsKey(refreshKey)); // let computation complete letGetFinishSignal.countDown(); getFinishedSignal.await(); checkNothingLogged(); // results should be visible assertEquals(2, cache.size()); assertEquals(getKey + suffix, map.get(getKey)); assertEquals(refreshKey + suffix, map.get(refreshKey)); assertEquals(2, cache.size()); } public void testInvalidateAndReloadDuringLoading() throws InterruptedException, ExecutionException { // computation starts; clear() is called, computation finishes final CountDownLatch computationStarted = new CountDownLatch(2); final CountDownLatch letGetFinishSignal = new CountDownLatch(1); final CountDownLatch getFinishedSignal = new CountDownLatch(4); final String getKey = "get"; final String refreshKey = "refresh"; final String suffix = "Suffix"; CacheLoader computeFunction = new CacheLoader() { @Override public String load(String key) throws InterruptedException { computationStarted.countDown(); letGetFinishSignal.await(); return key + suffix; } }; final LoadingCache cache = CacheBuilder.newBuilder().build(computeFunction); ConcurrentMap map = cache.asMap(); map.put(refreshKey, refreshKey); new Thread() { @Override public void run() { cache.getUnchecked(getKey); getFinishedSignal.countDown(); } }.start(); new Thread() { @Override public void run() { cache.refresh(refreshKey); getFinishedSignal.countDown(); } }.start(); computationStarted.await(); cache.invalidate(getKey); cache.invalidate(refreshKey); assertFalse(map.containsKey(getKey)); assertFalse(map.containsKey(refreshKey)); // start new computations new Thread() { @Override public void run() { cache.getUnchecked(getKey); getFinishedSignal.countDown(); } }.start(); new Thread() { @Override public void run() { cache.refresh(refreshKey); getFinishedSignal.countDown(); } }.start(); // let computation complete letGetFinishSignal.countDown(); getFinishedSignal.await(); checkNothingLogged(); // results should be visible assertEquals(2, cache.size()); assertEquals(getKey + suffix, map.get(getKey)); assertEquals(refreshKey + suffix, map.get(refreshKey)); } public void testExpandDuringLoading() throws InterruptedException { final int count = 3; final AtomicInteger callCount = new AtomicInteger(); // tells the computing thread when to start computing final CountDownLatch computeSignal = new CountDownLatch(1); // tells the main thread when computation is pending final CountDownLatch secondSignal = new CountDownLatch(1); // tells the main thread when the second get has started final CountDownLatch thirdSignal = new CountDownLatch(1); // tells the main thread when the third get has started final CountDownLatch fourthSignal = new CountDownLatch(1); // tells the test when all gets have returned final CountDownLatch doneSignal = new CountDownLatch(count); CacheLoader computeFunction = new CacheLoader() { @Override public String load(String key) throws InterruptedException { callCount.incrementAndGet(); secondSignal.countDown(); computeSignal.await(); return key + "foo"; } }; final LoadingCache cache = CacheBuilder.newBuilder().weakKeys().build(computeFunction); final AtomicReferenceArray result = new AtomicReferenceArray<>(count); final String key = "bar"; // start computing thread new Thread() { @Override public void run() { result.set(0, cache.getUnchecked(key)); doneSignal.countDown(); } }.start(); // wait for computation to start secondSignal.await(); // start waiting thread new Thread() { @Override public void run() { thirdSignal.countDown(); result.set(1, cache.getUnchecked(key)); doneSignal.countDown(); } }.start(); // give the second get a chance to run; it is okay for this to be racy // as the end result should be the same either way thirdSignal.await(); Thread.yield(); // Expand! CacheTesting.forceExpandSegment(cache, key); // start another waiting thread new Thread() { @Override public void run() { fourthSignal.countDown(); result.set(2, cache.getUnchecked(key)); doneSignal.countDown(); } }.start(); // give the third get a chance to run; it is okay for this to be racy // as the end result should be the same either way fourthSignal.await(); Thread.yield(); // let computation finish computeSignal.countDown(); doneSignal.await(); assertTrue(callCount.get() == 1); assertEquals("barfoo", result.get(0)); assertEquals("barfoo", result.get(1)); assertEquals("barfoo", result.get(2)); assertEquals("barfoo", cache.getUnchecked(key)); } // Test ignored because it is extremely flaky in CI builds public void ignoreTestExpandDuringRefresh() throws InterruptedException, ExecutionException { final AtomicInteger callCount = new AtomicInteger(); // tells the computing thread when to start computing final CountDownLatch computeSignal = new CountDownLatch(1); // tells the main thread when computation is pending final CountDownLatch secondSignal = new CountDownLatch(1); // tells the main thread when the second get has started final CountDownLatch thirdSignal = new CountDownLatch(1); // tells the main thread when the third get has started final CountDownLatch fourthSignal = new CountDownLatch(1); // tells the test when all gets have returned final CountDownLatch doneSignal = new CountDownLatch(3); final String suffix = "Suffix"; CacheLoader computeFunction = new CacheLoader() { @Override public String load(String key) throws InterruptedException { callCount.incrementAndGet(); secondSignal.countDown(); computeSignal.await(); return key + suffix; } }; final AtomicReferenceArray result = new AtomicReferenceArray<>(2); final LoadingCache cache = CacheBuilder.newBuilder().build(computeFunction); final String key = "bar"; cache.asMap().put(key, key); // start computing thread new Thread() { @Override public void run() { cache.refresh(key); doneSignal.countDown(); } }.start(); // wait for computation to start secondSignal.await(); checkNothingLogged(); // start waiting thread new Thread() { @Override public void run() { thirdSignal.countDown(); result.set(0, cache.getUnchecked(key)); doneSignal.countDown(); } }.start(); // give the second get a chance to run; it is okay for this to be racy // as the end result should be the same either way thirdSignal.await(); Thread.yield(); // Expand! CacheTesting.forceExpandSegment(cache, key); // start another waiting thread new Thread() { @Override public void run() { fourthSignal.countDown(); result.set(1, cache.getUnchecked(key)); doneSignal.countDown(); } }.start(); // give the third get a chance to run; it is okay for this to be racy // as the end result should be the same either way fourthSignal.await(); Thread.yield(); // let computation finish computeSignal.countDown(); doneSignal.await(); assertTrue(callCount.get() == 1); assertEquals(key, result.get(0)); assertEquals(key, result.get(1)); assertEquals(key + suffix, cache.getUnchecked(key)); } static Callable throwing(final Exception exception) { return new Callable() { @Override public T call() throws Exception { throw exception; } }; } }