Hot questions for Cache Builder in Guava

Top 10 Java Open Source / Guava / Cache Builder

Handle null value using Guava MapMaker/CacheBuilder

Question: I try to make a cache using MapMaker/CacheBuilder but I don't understand how to properly handle null values.

 ConcurrentMap<Key, Graph> graphs = new MapMaker()
       .concurrencyLevel(4)
       .weakKeys()
       .maximumSize(10000)
       .expireAfterWrite(10, TimeUnit.MINUTES)
       .makeComputingMap(
           new Function<Key, Graph>() {
             public Graph apply(Key key) {
               return createExpensiveGraph(key);
             }
           });

If the method createExpensiveGraph returns a null value, then a NullpointerException is thrown. I don't understand why the ComputingConcurrentHashMap throws a NPE instead of just returning a null value.

How to properly handle this ? Just catch the NPE and return null instead ? Am I missing something?

Answer: Guava tries to force you to avoid using null wherever possible, because improper or undocumented behavior in the presence of null can cause a huge amount of confusion and bugs. I think it's definitely a good idea to avoid using nulls wherever possible, and if you can modify your code so that it doesn't use null, I strongly recommend that approach instead.

The answer to your question critically depends on what a "null" value actually means in your application. Most likely, it means that there's "no value" for this key, or "nothing there." In this case, probably your best bet is to use Optional, wrapping non-null values with Optional.of and using Optional.absent() instead of null. If you must turn that into a null or non-null value, you can use Optional.orNull().


How does Guava expire entries in its CacheBuilder?

Question: I want to use a CacheBuilder

However I don't understand when Guava knows to expire entries.

How does Guava do it and what performance cost does it incurr?

Answer: The Guava Cache implementation expires entries in the course of normal maintenance operations, which occur on a per-segment basis during cache write operations and occasionally during cache read operations. Entries usually aren't expired at exactly their expiration time, just because Cache makes the deliberate decision not to create its own maintenance thread, but rather to let the user decide whether continuous maintenance is required.

I'm going to focus on expireAfterAccess, but the procedure for expireAfterWrite is almost identical. In terms of the mechanics, when you specify expireAfterAccess in the CacheBuilder, then each segment of the cache maintains a linked list access queue for entries in order from least-recent-access to most-recent-access. The cache entries are actually themselves nodes in the linked list, so when an entry is accessed, it removes itself from its old position in the access queue, and moves itself to the end of the queue.

When cache maintenance is performed, all the cache has to do is to expire every entry at the front of the queue until it finds an unexpired entry. This is straightforward and requires relatively little overhead, and it occurs in the course of normal cache maintenance. (Additionally, the cache deliberately limits the amount of work done in a single cleanup, minimizing the expense to any single cache operation.) Typically, the cost of cache maintenance is dominated by the expense of computing the actual entries in the cache.


How to mock Google Guava cache builder?

Question:

    @Component
    public class LibraryService {

        @Autowired
        private BookService bookService;

        private Cache<UUID, Book> bookCache = CacheBuilder.newBuilder().maximumSize(512).expireAfterWrite(15, TimeUnit.MINUTES).build();

        public void someMethod(UUID bookId) {
          try {
            Book book = bookCache.get(bookId, () -> bookService.findBookByUuid(bookId));
            //some operations
          } catch (ExecutionException e) {
            throw new ProcessingFailureException("Failed to load cache value", e);
          }

         }
    }

I need to write unit test for this class so that I tried to mock the Google Guava cache as following.

public class LibraryServiceTest {

    @InjectMocks
    private LibraryService service;

    @Mock
    private BookService bookService;

    @Mock
    private Cache<UUID, Book> bookCache;

    @Before
    public void initialize() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMethod() throws ExecutionException {
        UUID bookId = UUID.randomUUID();
        Book book = new Book();

        when(bookCache.get(bookId, () -> bookService.findBookByUuid(bookId))).thenReturn(book);

        service.someMethod(bookId);
    }
}

I got some NullPointer Exception.

com.google.common.util.concurrent.UncheckedExecutionException: java.lang.NullPointerException
    at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2203)
    at com.google.common.cache.LocalCache.get(LocalCache.java:3937)
    at com.google.common.cache.LocalCache$LocalManualCache.get(LocalCache.java:4739)
Caused by: java.lang.NullPointerException

Note: I know, I can change the method some testable way. In this case I couldn't do that.

Is there any way to mock this book cache?

Answer: Your code should work if you better mock the get method call as next to ensure arguments match such that you will get the expected book instance as result.

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;
...

public class LibraryServiceTest {
    ...

    @Test
    public void testMethod() throws ExecutionException {
        ...
        when(bookCache.get(eq(bookId), any(Callable.class))).thenReturn(book);
        ...
    }
}