ben-manes / caffeine

A high performance caching library for Java
Apache License 2.0
15.73k stars 1.59k forks source link

support for `V get(K key, Callable<? extends V> valueLoader)` in addition to current `get(K, Function<K..) ` #243

Closed alexeyOnGitHub closed 6 years ago

alexeyOnGitHub commented 6 years ago

my cache loader function needs to operate on a set of params that is different from the cache key. let's say the cache is defined as:

 Cache<UserId, User>

and my loading function for User (to use if there is a cache miss) is:

load(userId, requestContext)

this prevents me from using this method in com.github.benmanes.caffeine.cache.Cache class:

V get(@Nonnull K key, @Nonnull Function<? super K, ? extends V> mappingFunction);

since it requires [K] and Function[K] to be in sync. I can't use requestContext as a part of the key since it is different on every request (for audit and other purposes).

Guava's google cache supports a free-form value loader:

V get(K key, Callable<? extends V> valueLoader)

have you considered supporting this in Caffeine cache?

ben-manes commented 6 years ago

In this case you would use a "capturing lambda expression". That would be like using Guava's where the anonymous class captures the surrounding environment. So you could do,

cache.get(userId, key -> {
  return userService.get(userId, requestContext);
});

The benefit of non-capturing lamdas is that an instance can be cached by the JVM to avoid additional allocations. That limits its usefulness, so a capturing one is valid and the Function is merely to help give you the choice. The only annoyance is checked exceptions, but utilities like jOOL's Unchecked.function cover that pretty well.

ben-manes commented 6 years ago

You might be interested in https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood

alexeyOnGitHub commented 6 years ago

that would work, thank you! I may have over-simplified the source code example to get my point across. we are actually using async operations to load values from external systems, and I am wondering if I can use this "get with loader" approach with Async Cache. I see there is AsyncLoadingCache that works with java futures, but it requires declaring a loader when it is created. and regular Cache class only works with sync operations. if there a way to have something like AsyncCache with get(key, loader) operation?

I guess a workaround may be providing a dummy no-op AsyncCacheLoader when creating an instance using Caffeince...buildAsync() and then use a real loader in get(key, loader) operation. the downside would be that all get(key) calls will always return null and you need to remember to only use get(key, loader) for this cache.

ben-manes commented 6 years ago

Yeah, you can use a dummy loader and avoid using get(key). The get(key, func) should do what you want.

I didn't provide an AsyncCache initially so that I could understand why it might be helpful. The most common reason users use Cache instead of LoadingCache is poor - they do a racy getIfAbsent=>compute=>put instead of an atomic get that computes for them. For async code this is perhaps more valuable, since the code is more explicitly concurrent than the synchronous case.

I did layout the interface to make it easy to add AsyncCache later. I've seen a few integrations (e.g. Akka's) that would have wanted that, using a dummy loader as required, to bind to their caching APIs. There's been enough valid use-cases that I think it makes sense to add the interface now.

alexeyOnGitHub commented 6 years ago

I see the code I am working with also uses .getIfPresent() on that cache. with this workaround this method will always return false. I can change .getIfPresent() call to get(key, dummyLoader) - the idea is to return the value if present, and return null if not found (since the loader will be a NO-OP thingy).

implementing AsyncCache may be useful to skip these workarounds.

ben-manes commented 6 years ago

I think what you want is,

final AsyncLoadingCache<UUID, User> cache = Caffeine.newBuilder().buildAsync(key -> null);

public CompletableFuture<User> getUser(UUID userId, RequestContext requestContext) {
  return cache.get(userId, key -> userService.get(userId, requestContext));
}

The dummy loader would be for the builder and you'd avoid calling cache.get(key) which would invoke it.

alexeyOnGitHub commented 6 years ago

this would simplify things indeed. the AsyncLoadingCache constructed this way would return values if present and null otherwise, without trying to load them from another source. good enough for now. thanks a lot, Ben!

ben-manes commented 6 years ago

get(key) just calls get(key, func) with the attached loader, so it is mostly sugar. You only miss out on features like refresh and bulk loading, as would be expected.

I'll try to get around to AsyncCache to avoid this hack. Just never have the time to get through my backlog.