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 bb48189
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 16 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 @@ -15,6 +15,8 @@
*/
package com.github.benmanes.caffeine.guava;

import static java.util.Objects.requireNonNull;

import java.lang.reflect.Method;

import com.github.benmanes.caffeine.cache.Caffeine;
Expand All @@ -26,7 +28,7 @@
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,9 +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)
? new BulkLoader<>(castedLoader)
: new SingleLoader<>(castedLoader));
return build(builder, caffeinate(castedLoader));
}

/**
Expand All @@ -76,6 +76,21 @@ 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) {
requireNonNull(loader);
return hasLoadAll(loader)
? new BulkLoader<>(loader)
: new SingleLoader<>(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 @@ -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,23 @@ public Map<K, V> loadAll(Set<? extends K> 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,26 @@
*/
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.SingleLoader;
import com.github.benmanes.caffeine.guava.compatibility.TestingCacheLoaders;
import com.google.common.cache.CacheLoader;
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 +95,84 @@ public void testReload_throwable() {
}
}

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

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(SingleLoader.class);
assertThat(((SingleLoader<?, ?>) caffeine).cacheLoader).isSameInstanceAs(guava);

assertThat(caffeine.load(1)).isEqualTo(-1);
try {
caffeine.load(-1);
Assert.fail();
} catch (CacheLoaderException e) {
assertThat(e).hasCauseThat().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 void checkBulkLoader(Exception error,
com.github.benmanes.caffeine.cache.CacheLoader<Integer, Integer> caffeine) throws Exception {
assertThat(caffeine).isInstanceOf(BulkLoader.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 (CacheLoaderException e) {
assertThat(e).hasCauseThat().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 bb48189

Please sign in to comment.