From 0e7daa29e7c0a533b59506a192b9089b00e4472e Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Tue, 1 Dec 2015 18:43:16 -0800 Subject: [PATCH] NPE due to improperly scheduled write tasks (fixes #35) When an absent computation returns null, a add/removal task was being improperly scheduled. As there was no Node, this null field was then causing an NPE during the maintenance cycle. Usually that was on a background executor, but could be visible if `cleanUp` is called. This wasn't caught by tests since the executor's was swalled, logged, and logging was disabled to avoid spamming the report. Now the executor is instrumented so that the validation can assert no failures occurred. This detected the two bugs found with existing tests, so this provides more coverage than a one-off fix. --- .../caffeine/cache/BoundedLocalCache.java | 10 ++- .../benmanes/caffeine/cache/AsMapTest.java | 13 +++- .../caffeine/cache/AsyncLoadingCacheTest.java | 13 ++-- .../caffeine/cache/BoundedLocalCacheTest.java | 8 +-- .../benmanes/caffeine/cache/CacheTest.java | 2 + .../benmanes/caffeine/cache/EvictionTest.java | 72 ++++++++++--------- .../caffeine/cache/ExpirationTest.java | 4 +- .../caffeine/cache/ExpireAfterAccessTest.java | 18 +++-- .../caffeine/cache/ExpireAfterWriteTest.java | 18 +++-- .../caffeine/cache/LoadingCacheTest.java | 2 + .../caffeine/cache/RefreshAfterWriteTest.java | 18 +++-- .../cache/UnboundedLocalCacheTest.java | 5 +- .../caffeine/cache/testing/CacheContext.java | 5 +- .../caffeine/cache/testing/CacheSpec.java | 30 ++++---- .../testing/CacheValidationListener.java | 20 ++++++ .../cache/testing/GuavaCacheFromContext.java | 1 + .../cache/testing/TrackingExecutor.java | 71 ++++++++++++++++++ gradle/dependencies.gradle | 8 +-- 18 files changed, 223 insertions(+), 95 deletions(-) create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index ba4bc6c2dc..5ff31b1c03 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -698,8 +698,8 @@ boolean hasExpired(Node node, long now) { } /** - * Attempts to evict the entry based on the given removal cause. A removal due to expiration may - * be ignored if the entry was since updated and is no longer eligible for eviction. + * Attempts to evict the entry based on the given removal cause. A removal due to expiration or + * size may be ignored if the entry was since updated and is no longer eligible for eviction. * * @param node the entry to evict * @param cause the reason to evict @@ -1811,7 +1811,9 @@ V doComputeIfAbsent(K key, Object keyRef, Function mappi }); if (node == null) { - afterWrite(null, new RemovalTask(removed[0]), now); + if (removed[0] != null) { + afterWrite(null, new RemovalTask(removed[0]), now); + } return null; } if (cause[0] != null) { @@ -1978,6 +1980,8 @@ V remap(K key, Object keyRef, BiFunction rema if (removed[0] != null) { afterWrite(removed[0], new RemovalTask(removed[0]), now); + } else if (node == null) { + // absent and not computable } else if ((oldValue[0] == null) && (cause[0] == null)) { afterWrite(node, new AddTask(node, weight[1]), now); } else { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java index 4f17226990..6e89c619bb 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java @@ -1098,7 +1098,7 @@ public void compute_nullKey(Map map, CacheContext context) { @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void compute_nullMappingFunction(Map map, CacheContext context) { - map.computeIfPresent(1, null); + map.compute(1, null); } @CheckNoWriter @@ -1158,6 +1158,17 @@ public void compute_error(Map map, CacheContext context) { assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); } + @CheckNoWriter + @Test(dataProvider = "caches") + @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) + public void compute_absent_nullValue(Map map, CacheContext context) { + assertThat(map.compute(context.absentKey(), (key, value) -> null), is(nullValue())); + assertThat(context, both(hasMissCount(0)).and(hasHitCount(0))); + assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); + assertThat(map.get(context.absentKey()), is(nullValue())); + assertThat(map.size(), is(context.original().size())); + } + @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java index d50f006988..11647ae871 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsyncLoadingCacheTest.java @@ -37,7 +37,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; @@ -176,8 +175,7 @@ public void getFunc_absent_null_async(AsyncLoadingCache cache, ready.set(true); Awaits.await().untilTrue(done); - MoreExecutors.shutdownAndAwaitTermination( - (ExecutorService) context.executor(), 1, TimeUnit.MINUTES); + MoreExecutors.shutdownAndAwaitTermination(context.executor(), 1, TimeUnit.MINUTES); assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); @@ -216,8 +214,7 @@ public void getFunc_absent_failure_async(AsyncLoadingCache cac ready.set(true); Awaits.await().untilTrue(done); - MoreExecutors.shutdownAndAwaitTermination( - (ExecutorService) context.executor(), 1, TimeUnit.MINUTES); + MoreExecutors.shutdownAndAwaitTermination(context.executor(), 1, TimeUnit.MINUTES); assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); @@ -240,8 +237,7 @@ public void getFunc_absent_cancelled(AsyncLoadingCache cache, valueFuture.cancel(true); Awaits.await().untilTrue(done); - MoreExecutors.shutdownAndAwaitTermination( - (ExecutorService) context.executor(), 1, TimeUnit.MINUTES); + MoreExecutors.shutdownAndAwaitTermination(context.executor(), 1, TimeUnit.MINUTES); assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); @@ -445,8 +441,7 @@ public void get_absent_failure_async(AsyncLoadingCache cache, valueFuture.whenComplete((r, e) -> done.set(true)); Awaits.await().untilTrue(done); - MoreExecutors.shutdownAndAwaitTermination( - (ExecutorService) context.executor(), 1, TimeUnit.MINUTES); + MoreExecutors.shutdownAndAwaitTermination(context.executor(), 1, TimeUnit.MINUTES); assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java index 4748ff864f..249e352058 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java @@ -94,7 +94,7 @@ public void putWeighted_noOverflow() { @Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, - population = Population.FULL, maximumSize = MaximumSize.FULL, + population = Population.FULL, maximumSize = MaximumSize.FULL, executorMayFail = true, executor = CacheExecutor.REJECTING, removalListener = Listener.CONSUMING) public void evict_rejected(Cache cache, CacheContext context) { cache.put(context.absentKey(), context.absentValue()); @@ -283,7 +283,7 @@ public void exceedsMaximumBufferSize_onRead(Cache cache, Cache @Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = MaximumSize.FULL) - public void exceedsMaximumBufferSize_onWrite(Cache cache) { + public void exceedsMaximumBufferSize_onWrite(Cache cache, CacheContext context) { BoundedLocalCache localCache = asBoundedLocalCache(cache); Node dummy = localCache.nodeFactory.newNode(null, null, null, 1, 0); @@ -383,7 +383,7 @@ public void drain_onWrite(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = MaximumSize.FULL) - public void drain_nonblocking(Cache cache) { + public void drain_nonblocking(Cache cache, CacheContext context) { BoundedLocalCache localCache = asBoundedLocalCache(cache); AtomicBoolean done = new AtomicBoolean(); Runnable task = () -> { @@ -403,7 +403,7 @@ public void drain_nonblocking(Cache cache) { @Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = MaximumSize.FULL) - public void drain_blocksClear(Cache cache) { + public void drain_blocksClear(Cache cache, CacheContext context) { BoundedLocalCache localCache = asBoundedLocalCache(cache); checkDrainBlocks(localCache, localCache::clear); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java index 362f731c3a..7a0a4d4ea9 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CacheTest.java @@ -151,6 +151,8 @@ public void get_throwsException(Cache cache, CacheContext cont @Test(dataProvider = "caches") public void get_absent_null(LoadingCache cache, CacheContext context) { assertThat(cache.get(context.absentKey(), k -> null), is(nullValue())); + assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); + assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); } @CacheSpec diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java index 35b25acbd0..806da9f026 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/EvictionTest.java @@ -122,7 +122,7 @@ public void evict(Cache cache, CacheContext context, initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = MaximumSize.TEN, weigher = CacheWeigher.COLLECTION, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) public void evict_weighted(Cache> cache, - Eviction eviction, CacheContext context) { + CacheContext context, Eviction eviction) { @SuppressWarnings({"unchecked", "rawtypes"}) CacheWriter> writer = (CacheWriter) context.cacheWriter(); @@ -256,7 +256,8 @@ public void put_zeroWeight(Cache cache, CacheContext context) @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) - public void put(Cache> cache, Eviction eviction) { + public void put(Cache> cache, + CacheContext context, Eviction eviction) { cache.put("a", asList(1, 2, 3)); assertThat(cache.estimatedSize(), is(1L)); assertThat(eviction.weightedSize().getAsLong(), is(3L)); @@ -266,7 +267,8 @@ public void put(Cache> cache, Eviction eviction) { @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) - public void put_sameWeight(Cache> cache, Eviction eviction) { + public void put_sameWeight(Cache> cache, + CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); cache.put("a", asList(-1, -2, -3)); @@ -279,7 +281,7 @@ public void put_sameWeight(Cache> cache, Eviction ev weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) public void put_changeWeight(Cache> cache, - Eviction eviction, CacheContext context) { + CacheContext context, Eviction eviction) { @SuppressWarnings({"unchecked", "rawtypes"}) CacheWriter> writer = (CacheWriter) context.cacheWriter(); @@ -324,7 +326,8 @@ public void put_asyncWeight(AsyncLoadingCache> cache, @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) - public void replace_sameWeight(Cache> cache, Eviction eviction) { + public void replace_sameWeight(Cache> cache, + CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); cache.asMap().replace("a", asList(-1, -2, -3)); @@ -336,7 +339,8 @@ public void replace_sameWeight(Cache> cache, Eviction> cache, Eviction eviction) { + public void replace_changeWeight(Cache> cache, + CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); cache.asMap().replace("a", asList(-1, -2, -3, -4)); @@ -349,7 +353,7 @@ public void replace_changeWeight(Cache> cache, Eviction> cache, Eviction eviction) { + Cache> cache, CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); assertThat(cache.asMap().replace("a", asList(1, 2, 3), asList(4, 5, 6)), is(true)); @@ -362,7 +366,7 @@ public void replaceConditionally_sameWeight( weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) public void replaceConditionally_changeWeight( - Cache> cache, Eviction eviction) { + Cache> cache, CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); cache.asMap().replace("a", asList(1, 2, 3), asList(-1, -2, -3, -4)); @@ -375,7 +379,7 @@ public void replaceConditionally_changeWeight( weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) public void replaceConditionally_fails( - Cache> cache, Eviction eviction) { + Cache> cache, CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); assertThat(cache.asMap().replace("a", asList(1), asList(4, 5)), is(false)); @@ -387,7 +391,8 @@ public void replaceConditionally_fails( @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) - public void remove(Cache> cache, Eviction eviction) { + public void remove(Cache> cache, + CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); assertThat(cache.asMap().remove("a"), is(asList(1, 2, 3))); @@ -399,7 +404,8 @@ public void remove(Cache> cache, Eviction eviction) @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) - public void removeConditionally(Cache> cache, Eviction eviction) { + public void removeConditionally(Cache> cache, + CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); assertThat(cache.asMap().remove("a", asList(1, 2, 3)), is(true)); @@ -412,7 +418,7 @@ public void removeConditionally(Cache> cache, Eviction> cache, Eviction eviction) { + Cache> cache, CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); assertThat(cache.asMap().remove("a", asList(-1, -2, -3)), is(false)); @@ -424,7 +430,8 @@ public void removeConditionally_fails( @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.COLLECTION, population = Population.EMPTY, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) - public void invalidateAll(Cache> cache, Eviction eviction) { + public void invalidateAll(Cache> cache, + CacheContext context, Eviction eviction) { cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1))); cache.invalidateAll(); @@ -446,7 +453,8 @@ public void isWeighted(CacheContext context, Eviction eviction @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, weigher = CacheWeigher.TEN) - public void weightedSize(Cache cache, Eviction eviction) { + public void weightedSize(Cache cache, CacheContext context, + Eviction eviction) { assertThat(eviction.weightedSize().getAsLong(), is(10 * cache.estimatedSize())); } @@ -455,8 +463,8 @@ public void weightedSize(Cache cache, Eviction cache, CacheContext context, - Eviction eviction) { + public void maximumSize_decrease(Cache cache, + CacheContext context, Eviction eviction) { long newSize = context.maximumWeightOrSize() / 2; eviction.setMaximum(newSize); assertThat(eviction.getMaximum(), is(newSize)); @@ -474,8 +482,8 @@ public void maximumSize_decrease(Cache cache, CacheContext con @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, removalListener = { Listener.DEFAULT, Listener.CONSUMING }) - public void maximumSize_decrease_min(Cache cache, CacheContext context, - Eviction eviction) { + public void maximumSize_decrease_min(Cache cache, + CacheContext context, Eviction eviction) { eviction.setMaximum(0); assertThat(eviction.getMaximum(), is(0L)); if (context.initialSize() > 0) { @@ -488,8 +496,8 @@ public void maximumSize_decrease_min(Cache cache, CacheContext @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, removalListener = { Listener.DEFAULT, Listener.CONSUMING }) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void maximumSize_decrease_negative(Cache cache, CacheContext context, - Eviction eviction) { + public void maximumSize_decrease_negative(Cache cache, + CacheContext context, Eviction eviction) { try { eviction.setMaximum(-1); } finally { @@ -501,8 +509,8 @@ public void maximumSize_decrease_negative(Cache cache, CacheCo @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, removalListener = { Listener.DEFAULT, Listener.REJECTING }) - public void maximumSize_increase(Cache cache, CacheContext context, - Eviction eviction) { + public void maximumSize_increase(Cache cache, + CacheContext context, Eviction eviction) { eviction.setMaximum(2 * context.maximumWeightOrSize()); assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(eviction.getMaximum(), is(2 * context.maximumWeightOrSize())); @@ -511,8 +519,8 @@ public void maximumSize_increase(Cache cache, CacheContext con @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL, removalListener = Listener.REJECTING) - public void maximumSize_increase_max(Cache cache, CacheContext context, - Eviction eviction) { + public void maximumSize_increase_max(Cache cache, + CacheContext context, Eviction eviction) { eviction.setMaximum(Long.MAX_VALUE); assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(eviction.getMaximum(), is(Long.MAX_VALUE - Integer.MAX_VALUE)); // impl detail @@ -522,19 +530,19 @@ public void maximumSize_increase_max(Cache cache, CacheContext @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void coldest_unmodifiable(Eviction eviction) { + public void coldest_unmodifiable(CacheContext context, Eviction eviction) { eviction.coldest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void coldest_negative(Eviction eviction) { + public void coldest_negative(CacheContext context, Eviction eviction) { eviction.coldest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL) - public void coldest_zero(Eviction eviction) { + public void coldest_zero(CacheContext context, Eviction eviction) { assertThat(eviction.coldest(0), is(emptyMap())); } @@ -580,19 +588,19 @@ public void coldest_snapshot(Cache cache, CacheContext context @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void hottest_unmodifiable(Eviction eviction) { + public void hottest_unmodifiable(CacheContext context, Eviction eviction) { eviction.hottest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void hottest_negative(Eviction eviction) { + public void hottest_negative(CacheContext context, Eviction eviction) { eviction.hottest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, maximumSize = MaximumSize.FULL) - public void hottest_zero(Eviction eviction) { + public void hottest_zero(CacheContext context, Eviction eviction) { assertThat(eviction.hottest(0), is(emptyMap())); } @@ -617,8 +625,8 @@ public void hottest_order(CacheContext context, Eviction evict @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = MaximumSize.FULL) - public void hottest_snapshot(Cache cache, CacheContext context, - Eviction eviction) { + public void hottest_snapshot(Cache cache, + CacheContext context, Eviction eviction) { Map hottest = eviction.hottest(Integer.MAX_VALUE); cache.invalidateAll(); assertThat(hottest, is(equalTo(context.original()))); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java index 21253db44e..d515db43a3 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java @@ -87,7 +87,7 @@ public void expire_zero(Cache cache, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, writer = Writer.EXCEPTIONAL, requiresExpiration = true, - expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, executorMayFail = true, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, removalListener = Listener.REJECTING) public void getIfPresent_writerFails(Cache cache, CacheContext context) { @@ -837,7 +837,7 @@ public void computeIfPresent(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, requiresExpiration = true, executorMayFail = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java index 4fd9b05753..e44ef1af98 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java @@ -203,7 +203,7 @@ public void putIfAbsent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) - public void getExpiresAfter( + public void getExpiresAfter(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.getExpiresAfter(TimeUnit.MINUTES), is(1L)); } @@ -236,20 +236,22 @@ public void ageOf(CacheContext context, @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void oldest_unmodifiable( + public void oldest_unmodifiable(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.oldest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void oldest_negative(@ExpireAfterAccess Expiration expireAfterAccess) { + public void oldest_negative(CacheContext context, + @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.oldest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) - public void oldest_zero(@ExpireAfterAccess Expiration expireAfterAccess) { + public void oldest_zero(CacheContext context, + @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.oldest(0), is(emptyMap())); } @@ -285,20 +287,22 @@ public void oldest_snapshot(Cache cache, CacheContext context, @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void youngest_unmodifiable( + public void youngest_unmodifiable(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.youngest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void youngest_negative(@ExpireAfterAccess Expiration expireAfterAccess) { + public void youngest_negative(CacheContext context, + @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.youngest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) - public void youngest_zero(@ExpireAfterAccess Expiration expireAfterAccess) { + public void youngest_zero(CacheContext context, + @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.youngest(0), is(emptyMap())); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java index 09be14b20e..6cfb69162d 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java @@ -190,7 +190,7 @@ public void putIfAbsent(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) - public void getExpiresAfter( + public void getExpiresAfter(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { assertThat(expireAfterWrite.getExpiresAfter(TimeUnit.MINUTES), is(1L)); } @@ -223,20 +223,22 @@ public void ageOf(CacheContext context, @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void oldest_unmodifiable( + public void oldest_unmodifiable(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.oldest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void oldest_negative(@ExpireAfterWrite Expiration expireAfterWrite) { + public void oldest_negative(CacheContext context, + @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.oldest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) - public void oldest_zero(@ExpireAfterWrite Expiration expireAfterWrite) { + public void oldest_zero(CacheContext context, + @ExpireAfterWrite Expiration expireAfterWrite) { assertThat(expireAfterWrite.oldest(0), is(emptyMap())); } @@ -272,20 +274,22 @@ public void oldest_snapshot(Cache cache, CacheContext context, @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void youngest_unmodifiable( + public void youngest_unmodifiable(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.youngest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void youngest_negative(@ExpireAfterWrite Expiration expireAfterWrite) { + public void youngest_negative(CacheContext context, + @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.youngest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) - public void youngest_zero(@ExpireAfterWrite Expiration expireAfterWrite) { + public void youngest_zero(CacheContext context, + @ExpireAfterWrite Expiration expireAfterWrite) { assertThat(expireAfterWrite.youngest(0), is(emptyMap())); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java index b5c5f4f54c..e75c922085 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LoadingCacheTest.java @@ -71,6 +71,8 @@ public void get_null(LoadingCache cache, CacheContext context) @CacheSpec(loader = Loader.NULL) public void get_absent_null(LoadingCache cache, CacheContext context) { assertThat(cache.get(context.absentKey()), is(nullValue())); + assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); + assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); } @CheckNoWriter diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java index b166f3e9bc..c83750bd4d 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java @@ -203,7 +203,7 @@ public void getAll(AsyncLoadingCache cache, CacheContext conte @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) - public void getExpiresAfter( + public void getExpiresAfter(CacheContext context, @RefreshAfterWrite Expiration refreshAfterWrite) { assertThat(refreshAfterWrite.getExpiresAfter(TimeUnit.MINUTES), is(1L)); } @@ -232,20 +232,22 @@ public void ageOf(CacheContext context, @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void oldest_unmodifiable( + public void oldest_unmodifiable(CacheContext context, @RefreshAfterWrite Expiration refreshAfterWrite) { refreshAfterWrite.oldest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void oldest_negative(@RefreshAfterWrite Expiration refreshAfterWrite) { + public void oldest_negative(CacheContext context, + @RefreshAfterWrite Expiration refreshAfterWrite) { refreshAfterWrite.oldest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) - public void oldest_zero(@RefreshAfterWrite Expiration refreshAfterWrite) { + public void oldest_zero(CacheContext context, + @RefreshAfterWrite Expiration refreshAfterWrite) { assertThat(refreshAfterWrite.oldest(0), is(emptyMap())); } @@ -281,20 +283,22 @@ public void oldest_snapshot(Cache cache, CacheContext context, @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) - public void youngest_unmodifiable( + public void youngest_unmodifiable(CacheContext context, @RefreshAfterWrite Expiration refreshAfterWrite) { refreshAfterWrite.youngest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) - public void youngest_negative(@RefreshAfterWrite Expiration refreshAfterWrite) { + public void youngest_negative(CacheContext context, + @RefreshAfterWrite Expiration refreshAfterWrite) { refreshAfterWrite.youngest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) - public void youngest_zero(@RefreshAfterWrite Expiration refreshAfterWrite) { + public void youngest_zero(CacheContext context, + @RefreshAfterWrite Expiration refreshAfterWrite) { assertThat(refreshAfterWrite.youngest(0), is(emptyMap())); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/UnboundedLocalCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/UnboundedLocalCacheTest.java index 09b3935ee0..efeb09d394 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/UnboundedLocalCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/UnboundedLocalCacheTest.java @@ -23,6 +23,7 @@ import org.testng.annotations.Listeners; import org.testng.annotations.Test; +import com.github.benmanes.caffeine.cache.testing.CacheContext; import com.github.benmanes.caffeine.cache.testing.CacheProvider; import com.github.benmanes.caffeine.cache.testing.CacheSpec; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher; @@ -48,7 +49,7 @@ public final class UnboundedLocalCacheTest { refreshAfterWrite = Expire.DISABLED, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) @Test(dataProvider = "caches") - public void noPolicy(Cache cache) { + public void noPolicy(Cache cache, CacheContext context) { assertThat(cache.policy().eviction(), is(Optional.empty())); assertThat(cache.policy().expireAfterWrite(), is(Optional.empty())); assertThat(cache.policy().expireAfterAccess(), is(Optional.empty())); @@ -61,7 +62,7 @@ public void noPolicy(Cache cache) { refreshAfterWrite = Expire.DISABLED, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) @Test(dataProvider = "caches") - public void noPolicy_async(AsyncLoadingCache cache) { + public void noPolicy_async(AsyncLoadingCache cache, CacheContext context) { assertThat(cache.synchronous().policy().eviction(), is(Optional.empty())); assertThat(cache.synchronous().policy().expireAfterWrite(), is(Optional.empty())); assertThat(cache.synchronous().policy().expireAfterAccess(), is(Optional.empty())); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java index af03ed1d7f..9f988064a3 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Executor; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.Nullable; @@ -66,12 +65,12 @@ public final class CacheContext { final CacheExecutor cacheExecutor; final ReferenceType valueStrength; final ReferenceType keyStrength; + final TrackingExecutor executor; final MaximumSize maximumSize; final Population population; final CacheWeigher weigher; final Expire afterAccess; final Expire afterWrite; - final Executor executor; final Compute compute; final Advance advance; final Expire refresh; @@ -354,7 +353,7 @@ public CacheExecutor executorType() { return cacheExecutor; } - public Executor executor() { + public TrackingExecutor executor() { return executor; } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java index 532fd7061f..fed326be07 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java @@ -25,10 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -483,36 +480,41 @@ CacheExecutor[] executor() default { CacheExecutor.DIRECT, }; + /** If the executor is allowed to have failures. */ + boolean executorMayFail() default false; + /** The executors that the cache can be configured with. */ - enum CacheExecutor implements Supplier { + enum CacheExecutor implements Supplier { DEFAULT { // fork-join common pool - @Override public Executor get() { + @Override public TrackingExecutor get() { // Use with caution as may be unpredictable during tests if awaiting completion - return ForkJoinPool.commonPool(); + return null; } }, DIRECT { - @Override public Executor get() { + @Override public TrackingExecutor get() { // Cache implementations must avoid deadlocks by incorrectly assuming async execution - return MoreExecutors.directExecutor(); + return new TrackingExecutor(MoreExecutors.newDirectExecutorService()); } }, SINGLE { - @Override public Executor get() { + @Override public TrackingExecutor get() { // Isolated to the test execution - may be shutdown by test to assert completion - return Executors.newSingleThreadExecutor( - new ThreadFactoryBuilder().setDaemon(true).build()); + return new TrackingExecutor(Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setDaemon(true).build())); } }, REJECTING { - @Override public Executor get() { + @Override public TrackingExecutor get() { // Cache implementations must avoid corrupting internal state due to rejections - return runnable -> { throw new RejectedExecutionException(); }; + TrackingExecutor executor = CacheExecutor.DIRECT.get(); + executor.shutdown(); + return executor; } }; @Override - public abstract Executor get(); + public abstract TrackingExecutor get(); } /* ---------------- Populated -------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java index 91704d69d8..171312ba48 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java @@ -24,6 +24,7 @@ import static com.github.benmanes.caffeine.cache.testing.HasStats.hasLoadSuccessCount; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasMissCount; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -67,6 +68,7 @@ public void afterInvocation(IInvokedMethod method, ITestResult testResult) { } checkWriter(testResult, context); checkNoStats(testResult, context); + checkExecutor(testResult, context); } } catch (Throwable caught) { testResult.setStatus(ITestResult.FAILURE); @@ -76,6 +78,23 @@ public void afterInvocation(IInvokedMethod method, ITestResult testResult) { } } + /** Checks whether the {@link TrackingExecutor} had unexpected failures. */ + private static void checkExecutor(ITestResult testResult, CacheContext context) { + Method testMethod = testResult.getMethod().getConstructorOrMethod().getMethod(); + CacheSpec cacheSpec = testMethod.getAnnotation(CacheSpec.class); + if (cacheSpec == null) { + return; + } + + assertThat("CacheContext required", context, is(not(nullValue()))); + TrackingExecutor executor = context.executor(); + if (cacheSpec.executorMayFail()) { + assertThat(executor.failureCount(), is(greaterThan(0))); + } else { + assertThat(executor.failureCount(), is(0)); + } + } + /** Checks the writer if {@link CheckNoWriter} is found. */ private static void checkWriter(ITestResult testResult, CacheContext context) { Method testMethod = testResult.getMethod().getConstructorOrMethod().getMethod(); @@ -83,6 +102,7 @@ private static void checkWriter(ITestResult testResult, CacheContext context) { if (checkWriter == null) { return; } + assertThat("Test requires CacheContext param for validation", context, is(not(nullValue()))); verifyWriter(context, (verifier, writer) -> verifier.zeroInteractions()); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java index df369a199a..93eef21b2e 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java @@ -329,6 +329,7 @@ public V compute(K key, BiFunction remappingF statsCounter.recordLoadException(ticker.read() - now); return null; } else { + statsCounter.recordLoadException(ticker.read() - now); return null; } } else { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java new file mode 100644 index 0000000000..585444b47e --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Ben Manes. All Rights Reserved. + * + * 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.github.benmanes.caffeine.cache.testing; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.util.concurrent.ForwardingExecutorService; + +/** + * An executor that retains metrics regarding the execution history. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class TrackingExecutor extends ForwardingExecutorService { + private final ExecutorService delegate; + private final AtomicInteger totalTasks; + private final AtomicInteger failures; + + public TrackingExecutor(ExecutorService executor) { + delegate = requireNonNull(executor); + totalTasks = new AtomicInteger(); + failures = new AtomicInteger(); + } + + @Override + public void execute(Runnable command) { + totalTasks.incrementAndGet(); + Runnable submit = makeFailureAware(() -> delegate.execute(makeFailureAware(command))); + submit.run(); + } + + private Runnable makeFailureAware(Runnable task) { + return () -> { + try { + task.run(); + } catch (Throwable t) { + failures.incrementAndGet(); + throw t; + } + }; + } + + public int totalTasksCount() { + return totalTasks.get(); + } + + public int failureCount() { + return failures.get(); + } + + @Override + protected ExecutorService delegate() { + return delegate; + } +} diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index fc415ebf44..44d313f367 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -40,7 +40,7 @@ ext { ycsb: '0.5.0', ] test_versions = [ - awaitility: '1.6.5', + awaitility: '1.7.0', easymock: '3.4', hamcrest: '2.0.0.0', jcache_tck: '1.0.1', @@ -58,8 +58,8 @@ ext { ehcache2: '2.10.1-55', ehcache3: '3.0.0.m4', high_scale_lib: '1.0.6', - infinispan: '8.1.0.Beta1', - jackrabbit: '1.3.10', + infinispan: '8.1.0.CR1', + jackrabbit: '1.3.11', jamm: '0.3.1', java_object_layout: '0.3.2', koloboke: '0.6.8', @@ -67,7 +67,7 @@ ext { tcache: '0.4.0', ] plugin_versions = [ - checkstyle: '6.7', + checkstyle: '6.13', coveralls: '2.4.0', extra_conf: '3.0.3', error_prone: '0.0.8',