Skip to content

Commit

Permalink
Add utility to adapt a Guava CacheLoader to Caffeine's (fixes #766)
Browse files Browse the repository at this point in the history
The Guava adapters wrap the Caffeine implementations to masquerade under
their APIs. Sometimes users wish to use Caffeine's APIs without migrating
their Guava CacheLoader. The adapter is now available for use with our
cache builder.

```java
CacheLoader<K, V> caffeineLoader = CaffeinatedGuava.caffeinate(guavaLoader);
LoadingCache<K, V> caffeineCache = Caffeine.newBuilder().build(caffeineLoader);
```
  • Loading branch information
ben-manes committed Sep 4, 2022
1 parent f0a47d5 commit 9fd52f6
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 15 deletions.
5 changes: 5 additions & 0 deletions config/spotbugs/exclude.xml
Expand Up @@ -259,6 +259,11 @@
</Or>
<Bug pattern="DCN_NULLPOINTER_EXCEPTION"/>
</Match>
<Match>
<Class name="com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache$SingleLoader"/>
<Method name="asyncReload"/>
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
</Match>

<!-- JCache -->
<Match>
Expand Down
Expand Up @@ -19,14 +19,16 @@

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.BulkLoader;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.ExternalizedBulkLoader;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.ExternalizedSingleLoader;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.SingleLoader;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.errorprone.annotations.CheckReturnValue;

/**
* An adapter to expose a Caffeine cache through the Guava interfaces.
* Static utility methods pertaining to adapting between Caffeine and Guava cache interfaces.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
Expand Down Expand Up @@ -57,7 +59,7 @@ public static <K, V, K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
Caffeine<K, V> builder, CacheLoader<? super K1, V1> loader) {
@SuppressWarnings("unchecked")
CacheLoader<K1, V1> castedLoader = (CacheLoader<K1, V1>) loader;
return build(builder, hasLoadAll(castedLoader)
return build(builder, hasLoadAll(loader)
? new BulkLoader<>(castedLoader)
: new SingleLoader<>(castedLoader));
}
Expand All @@ -76,6 +78,20 @@ public static <K, V, K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
return new CaffeinatedGuavaLoadingCache<>(builder.build(loader));
}

/**
* Returns a Caffeine cache loader that delegates to a Guava cache loader.
*
* @param loader the cache loader used to obtain new values
* @return a cache loader exposed under the Caffeine APIs
*/
@CheckReturnValue
public static <K, V> com.github.benmanes.caffeine.cache.CacheLoader<K, V> caffeinate(
CacheLoader<K, V> loader) {
return hasLoadAll(loader)
? new ExternalizedBulkLoader<>(loader)
: new ExternalizedSingleLoader<>(loader);
}

static boolean hasLoadAll(CacheLoader<?, ?> cacheLoader) {
return hasMethod(cacheLoader, "loadAll", Iterable.class);
}
Expand Down
Expand Up @@ -21,15 +21,20 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;

import org.checkerframework.checker.nullness.qual.Nullable;

import com.github.benmanes.caffeine.cache.CacheLoader;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ExecutionError;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.UncheckedExecutionException;

/**
Expand Down Expand Up @@ -133,7 +138,7 @@ static class SingleLoader<K, V> implements CacheLoader<K, V>, Serializable {
}

@Override
public V load(K key) {
public V load(K key) throws Exception {
try {
V value = cacheLoader.load(key);
if (value == null) {
Expand All @@ -151,20 +156,19 @@ public V load(K key) {
}

@Override
public V reload(K key, V oldValue) {
public CompletableFuture<V> asyncReload(K key, V oldValue, Executor executor) {
var future = new CompletableFuture<V>();
try {
V value = Futures.getUnchecked(cacheLoader.reload(key, oldValue));
if (value == null) {
throw new InvalidCacheLoadException("null value");
ListenableFuture<V> reload = cacheLoader.reload(key, oldValue);
if (reload == null) {
future.completeExceptionally(new InvalidCacheLoadException("null value"));
} else {
Futures.addCallback(reload, new FutureCompleter<>(future), Runnable::run);
}
return value;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CacheLoaderException(e);
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
} catch (Throwable t) {
future.completeExceptionally(t);
}
return future;
}
}

Expand Down Expand Up @@ -201,4 +205,45 @@ public Map<K, V> loadAll(Set<? extends K> keys) {
}
}
}

static class ExternalizedSingleLoader<K, V> extends SingleLoader<K, V> {
ExternalizedSingleLoader(com.google.common.cache.CacheLoader<K, V> cacheLoader) {
super(cacheLoader);
}
@Override public V load(K key) throws Exception {
V value = cacheLoader.load(key);
if (value == null) {
throw new InvalidCacheLoadException("null value");
}
return value;
}
}

static final class ExternalizedBulkLoader<K, V> extends ExternalizedSingleLoader<K, V> {
ExternalizedBulkLoader(com.google.common.cache.CacheLoader<K, V> cacheLoader) {
super(cacheLoader);
}
@Override public Map<K, V> loadAll(Set<? extends K> keys) throws Exception {
return cacheLoader.loadAll(keys);
}
}

static final class FutureCompleter<V> implements FutureCallback<V> {
final CompletableFuture<V> future;

FutureCompleter(CompletableFuture<V> future) {
this.future = future;
}

@Override public void onSuccess(@Nullable V value) {
if (value == null) {
future.completeExceptionally(new InvalidCacheLoadException("null value"));
} else {
future.complete(value);
}
}
@Override public void onFailure(Throwable t) {
future.completeExceptionally(t);
}
}
}
Expand Up @@ -15,13 +15,29 @@
*/
package com.github.benmanes.caffeine.guava;

import static com.google.common.truth.Truth.assertThat;

import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletionException;

import org.junit.Assert;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaCache.CacheLoaderException;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.BulkLoader;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.ExternalizedBulkLoader;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.ExternalizedSingleLoader;
import com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.SingleLoader;
import com.github.benmanes.caffeine.guava.compatibility.TestingCacheLoaders;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.testing.SerializableTester;
import com.google.common.util.concurrent.MoreExecutors;

Expand Down Expand Up @@ -82,6 +98,127 @@ public void testReload_throwable() {
}
}

public void testCacheLoader_null() throws Exception {
try {
CaffeinatedGuava.caffeinate(null);
Assert.fail();
} catch (NullPointerException expected) {}

try {
var caffeine = CaffeinatedGuava.caffeinate(CacheLoader.from(key -> null));
caffeine.load(1);
Assert.fail();
} catch (InvalidCacheLoadException expected) {}
}

public void testCacheLoader_exception() throws Exception {
runCacheLoaderExceptionTest(new InterruptedException());
runCacheLoaderExceptionTest(new RuntimeException());
runCacheLoaderExceptionTest(new Exception());
}

public void runCacheLoaderExceptionTest(Exception error) throws Exception {
var guava = new CacheLoader<Integer, Integer>() {
@Override public Integer load(Integer key) throws Exception {
throw error;
}
@Override public ImmutableMap<Integer, Integer> loadAll(
Iterable<? extends Integer> keys) throws Exception {
throw error;
}
};
var caffeine = CaffeinatedGuava.caffeinate(guava);
try {
caffeine.load(1);
Assert.fail();
} catch (Exception e) {
assertThat(e).isSameInstanceAs(error);
}
try {
caffeine.loadAll(Set.of(1, 2));
Assert.fail();
} catch (Exception e) {
assertThat(e).isSameInstanceAs(error);
}
try {
caffeine.asyncReload(1, 2, Runnable::run).join();
Assert.fail();
} catch (CompletionException e) {
assertThat(e).hasCauseThat().isSameInstanceAs(error);
}
}

public void testCacheLoader_single() throws Exception {
var error = new Exception();
var guava = new CacheLoader<Integer, Integer>() {
@Override public Integer load(Integer key) throws Exception {
if (key > 0) {
return -key;
}
throw error;
}
};
var caffeine = CaffeinatedGuava.caffeinate(guava);
assertThat(caffeine).isNotInstanceOf(BulkLoader.class);
checkSingleLoader(error, guava, caffeine);
}

public void testCacheLoader_bulk() throws Exception {
var error = new Exception();
var guava = new CacheLoader<Integer, Integer>() {
@Override public Integer load(Integer key) throws Exception {
if (key > 0) {
return -key;
}
throw error;
}
@Override public ImmutableMap<Integer, Integer> loadAll(
Iterable<? extends Integer> keys) throws Exception {
if (Iterables.all(keys, key -> key > 0)) {
return Maps.toMap(ImmutableSet.copyOf(keys), key -> -key);
}
throw error;
}
};
var caffeine = CaffeinatedGuava.caffeinate(guava);
checkSingleLoader(error, guava, caffeine);
checkBulkLoader(error, caffeine);
}

private static void checkSingleLoader(Exception error, CacheLoader<Integer, Integer> guava,
com.github.benmanes.caffeine.cache.CacheLoader<Integer, Integer> caffeine) throws Exception {
assertThat(caffeine).isInstanceOf(ExternalizedSingleLoader.class);
assertThat(((SingleLoader<?, ?>) caffeine).cacheLoader).isSameInstanceAs(guava);

assertThat(caffeine.load(1)).isEqualTo(-1);
try {
caffeine.load(-1);
Assert.fail();
} catch (Exception e) {
assertThat(e).isSameInstanceAs(error);
}

assertThat(caffeine.asyncReload(1, 2, Runnable::run).join()).isEqualTo(-1);
try {
caffeine.asyncReload(-1, 2, Runnable::run).join();
Assert.fail();
} catch (CompletionException e) {
assertThat(e).hasCauseThat().isSameInstanceAs(error);
}
}

private static void checkBulkLoader(Exception error,
com.github.benmanes.caffeine.cache.CacheLoader<Integer, Integer> caffeine) throws Exception {
assertThat(caffeine).isInstanceOf(ExternalizedBulkLoader.class);
assertThat(caffeine.loadAll(Set.of(1, 2, 3))).isEqualTo(Map.of(1, -1, 2, -2, 3, -3));
try {
caffeine.loadAll(Set.of(1, -1));
Assert.fail();
} catch (Exception e) {
assertThat(e).isSameInstanceAs(error);
}
}

enum IdentityLoader implements com.github.benmanes.caffeine.cache.CacheLoader<Object, Object> {
INSTANCE;

Expand Down
Expand Up @@ -36,6 +36,7 @@
*
* @author mike nonemacher
*/
@SuppressWarnings("CanIgnoreReturnValueSuggester")
class CacheBuilderFactory {
// Default values contain only 'null', which means don't call the CacheBuilder method (just give
// the CacheBuilder default).
Expand Down

0 comments on commit 9fd52f6

Please sign in to comment.