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 70de16f
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 15 deletions.
Original file line number Diff line number Diff line change
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 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
Original file line number Diff line number Diff line change
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);
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 70de16f

Please sign in to comment.