question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

LoadingCache.getIfPresent(key) returns null after LoadingCache.getUnchecked(key) returned non null value

See original GitHub issue

I am using the LoadingCache in a highly concurrent system and I observed some behavior that I am not sure is intended (to me it looks like a bug 😃 ). I wrote the following sample code to illustrate the problem :

public static void main(String[] args) {
        final LoadingCache<String, Object> cache = 
            CacheBuilder.newBuilder()
                                 .expireAfterAccess(65, TimeUnit.MINUTES)
                                 .build(new CacheLoader<String, Object>(){

                                                    @Override
                                                     public Object load(String arg0) throws Exception {
                                                                Object obj = new Object();
                                                                    System.out.println("creating object: " + obj);
                                                                    return obj;
                                                     }
                                  });


        int threadCount = 600;
        final String key = "hello/world";
        Runnable task = new Runnable() {

            @Override
            public void run() {
                try {
                    Object valueFromUnchecked = cache.getUnchecked(key);
                    if (valueFromUnchecked == null) {
                        System.out.println(Thread.currentThread().getName() + " valueFromUnchecked is null!!!");
                    }

                    Object value = cache.getIfPresent(key);
                    if (value == null) {
                        System.out.println(Thread.currentThread().getName() + " value is null!!!");
                    }

                    if (value != valueFromUnchecked) {
                        System.out.println(String.format(Thread.currentThread().getName() + "valueFromUnchecked:%s, value:%s", valueFromUnchecked, value));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < threadCount; i++) {
            Thread t = new Thread(task);
            t.setName("thread-" + i);
            t.start();
        }

Sometimes thing are fine, I don’t get the output of “<thread name> value is null!!!” lines and some times I do. I never get the output of “valueFromUnchecked from getUnchecked is null!!!”

My understanding of from LoadingCache java doc, is that if getUnchecked() does not return null then getIfPresent() shouldn’t return null as well; given that the cache has not expired yet.

Issue Analytics

  • State:open
  • Created 8 years ago
  • Comments:15 (11 by maintainers)

github_iconTop GitHub Comments

2reactions
ben-manescommented, Feb 2, 2022

Unfortunately I don’t have the time to assist in maintaining Guava’s Cache, and like the other coauthors, have moved on. I would prefer to have the difference between libraries be primarily performance and features for new code, rather than have users deal with bugs in their existing code. I can help review patches if anyone is interested in sending bug fixes.

I am familiar with the code base but Charles and I inherited it from Bob, where it was already very complex by the time we started adding / rewriting the caching logic. Unfortunately we didn’t realize the linearization problems early enough so there were some original sins, likely further mistakes introduced later on, and we were both 20%ers (with Charles going full time for a short period). For example how to handle explicit writes for an in-flight load is not linearized (puts stomp the load whereas removals no-op, iirc). When writing narrow portions from the perspective of a transient best-effort cache the issues seemed benign, but taking a step back it’s clearly wrong. The overwhelming amount of code due to forking ConcurrentHashMap makes it hard to wade through. Caffeine benefits from decorating the map, the addition of Java 8’s computes, being much more test driven, and learning from these past experiences.

I wrote a simple Lincheck test below that could be extended and drive iterations on fixes for these linearization problems. However as @cgdecker mentioned since this cache is in maintenance mode, if possible then please try Caffeine where I’ve tried to be more diligent on correctness.

Lincheck Test
import org.jetbrains.kotlinx.lincheck.LinChecker;
import org.jetbrains.kotlinx.lincheck.annotations.Operation;
import org.jetbrains.kotlinx.lincheck.annotations.Param;
import org.jetbrains.kotlinx.lincheck.paramgen.IntGen;
import org.jetbrains.kotlinx.lincheck.strategy.stress.StressOptions;
import org.jetbrains.kotlinx.lincheck.verifier.VerifierState;
import org.testng.annotations.Test;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

@Param(name = "key", gen = IntGen.class, conf = "1:10")
public final class GuavaLincheckTest extends VerifierState {
  private final LoadingCache<Integer, Integer> cache;

  public GuavaLincheckTest() {
    cache = CacheBuilder.newBuilder().build(CacheLoader.from(k -> -k));
  }

  @Operation
  public Integer getIfPresent(@Param(name = "key") int key) {
    return cache.getIfPresent(key);
  }

  @Operation
  public Integer getUnchecked(@Param(name = "key") int key) {
    return cache.getUnchecked(key);
  }

  @Test
  public void stressTest() {
    var options = new StressOptions()
        .iterations(100)
        .invocationsPerIteration(10_000);
    new LinChecker(getClass(), options).check();
  }

  @Override
  protected Object extractState() {
    return cache.asMap();
  }
}
1reaction
cgdeckercommented, Feb 2, 2022

I think it’s unlikely we’ll fix this at this point, though if someone wants to contribute a fix we’d be happy to look at that.

Note that we recommend people use Caffeine instead of Guava’s cache; it’s based on Guava’s cache APIs but maintained by @ben-manes who has far more caching expertise than anyone on the Guava team these days.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to avoid caching when values are null? - Stack Overflow
My problem is when the data does not exists in database, I want it to return null and to not do any caching....
Read more >
How to handle "null" value in CacheLoader.load(key) ?
I know that in CacheLoader.load(key) method we should not return a "null", the javadoc ... "InvalidCacheLoadException" when I tried to return a null...
Read more >
LoadingCache (Guava: Google Core Libraries for Java 19.0 API)
Returns a map of the values associated with keys , creating or retrieving those values if necessary. The returned map contains entries that...
Read more >
Guava Cache - Baeldung
Notice how there's no value in the cache for our “hello” key, ... key, String value) { return value.length(); } }; LoadingCache<String, ...
Read more >
com.google.common.cache.LoadingCache.getIfPresent java ...
get · getUnchecked. Returns the value associated with key in this cache, first loading that value if necessary. No obser · invalidateAll ·...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found