1

I am trying to use Google's Guava Cache and I am just testing it with expireAfterWrite with 1 minute intervals. In the application I am working on there are hundreds of users. So my understanding is that when the first person accesses the cache, we fill it from the database and then the one minute timer should start counting down. Now that the cache is filled. We will always get it from the cache for that one minute period. After one minute, it will expire and we get it from the database again.

However, it appears that everytime I access the cache within that one minute period, the timer resets. Why is that? If I don't access the cache within the one minute period the cache expires and works fine.

Code:

 Cache<Long, List<someObj>> cacheMap = CacheBuilder.newBuilder().maximumSize(maxSize).expireAfterWrite(maxTime, TimeUnit.MINUTES).removalListener(new RemovalListener<Long, List<someObj>>() {
    public void onRemoval(RemovalNotification<Long, List<someObj>> notification) {
        if (notification.getCause() == RemovalCause.EXPIRED) {
            logger.info("Value " + notification.getValue() + " has been expired");
        } else {
            logger.info("Just removed for some reason");
        }
    }
}).build();

...
//Convert to ConcurrentHashMap
cacheMap = someCache.asMap();

...


public List<someObj> getAllData(final Long orgId, final User user) {
    // Get requests from cache
    List<Request> cacheList = overdueSlaMap.get(orgId);

    // Fill cached requests if null
    cacheList = fillCacheIfNecessary(orgId, cacheList, overdueSlaMap);  

    return cacheList;
}

//Strategy for reading and writing to cache
//This method is fine. On first load, cache is empty so we fill it from database. Wait one minute, value expires and cache is empty again so we get from cache. 
//However, everytime I refresh the page the one minute timer starts again. Surely, the one minute timer within the Guava Cache should not refresh regardless of how many times I access the cache within that one minute period. 
 private List<someObj> fillCacheIfNecessary(List<someObj> cacheList, ConcurrentMap<Long, List<someObj>> cacheMap) {

        // If the cache is empty then we will try to fill it from the database.
        if (cacheList == null) {
            logger.info("populating cache");
            List<someObj> objList = new ArrayList<someObj>();

            // if bla bla
            if (such and such) {
                objList = service.getFromDatabase(someArg);
            } else {
                objList = service.getFromDatabase(anotherarg);
            }

            // Concurrently lock the new cacheList retrieved from the database.
            List<someObj> newValue = Collections.unmodifiableList(objList);

            // Only insert if new value does not exist in cache map
            List<someObj> oldValue = cacheMap.putIfAbsent(orgId, newValue);

            // If old value already exists then we use the old value otherwise we use the new value
            cacheList = ((oldValue != null && !oldValue.isEmpty()) ? oldValue : newValue);
        }
        return cacheList;

    }

EDIT

I am calling the cache from a controller:

public ModelAndView getNotifications(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
    User user = getCurrentUser();
    CacheManager cacheManager = new CacheManager();
    List<someObj> objList = new ArrayList<Request>();

    // Get from database
    if (bla bla) {
        objList = getFromDatabase(user);
    }
    // Get from cache
    else {
        Long orgId = user.getUserOrganisation().getId();
        objList = cacheManager.getAllData(orgId, user);      
    }

    return new ModelAndView(SOMEVIEW);
}

which passes in useful data that is needed to the methods which passes it on the the method to call the database. However, I am now tryin to refactor my code to use LoadingCache but this requires me to override the load() method which only uses one parameter which is the key. I need more than just the key and I need to call the cacheManager in the controller, so it calls the appropriate methods. What is the load method? When is it used? When is it called and how often? Does it invalidate my other methods?

Johnathan Au
  • 5,244
  • 18
  • 70
  • 128

1 Answers1

1

I believe your issue is that you are using the Map returned from asMap as your cache. asMap is documented as Returns a view of the entries stored in this cache as a thread-safe map. There is nothing here that suggests you can use the Map as your cache. I suggest updating your code to use the Cache API and call put or get(key, Callable) on the cache directly.

You might want to consider using a LoadingCache as I have found that the API is simpler.

EDIT

So first, the load method is only called when the key does not exist in the cache. The cache calls the load method to get the value that should be returned for they key and then caches that value for use next time. One of the nice things about using a CacheLoader or a Callable is that the call to retrieve the data is thread-safe in that it is only called once per key even if multiple calls for the same key occur at the same time.

If you need to pass more information, create a wrapper object as such:

 public class MyKey{
       final Long orgId; // true key value
       final User user; // other values needed

       // implement equals and hashCode to ONLY use orgId. This will ensure that
       // the same cached value is returned for all requests with the same
       // orgId which is the ACTUAL key.
  }

You can also do the same thing using a regular Cache and create a Callable wrapping the values needed.

I would suggest that the CacheLoader invalidates fillCacheIfNecessary. The simple call to cache.get() will determine internally if the value must be retrieved or already exists in the cache. It is doing the get, check if null, retrieve work for you.

John B
  • 32,493
  • 6
  • 77
  • 98
  • But it also says in the JavaDocs, "Modifications made to the map directly affect the cache." – Johnathan Au Nov 13 '14 at 13:45
  • Sorry, you are right. However, give the LoadingCache or `get(key, Callable)` a try as it is the preferred method. Let me know if that doesn't work. – John B Nov 13 '14 at 13:53
  • Hi, I'm trying to refactor my code to use LoadingCache but one of the problems I've ran into is that I have to override the load method inside CacheBuilder. First of all, when is the load method called? What is it used for? If I MUST implement it, how can I pass in more than parameter through the load method? As you can see in my code, if I was to retrieve an object from the database, I do not rely on just the object's id or key but on a bunch of other things. Does the load method invalidate my fillCacheIfNecessary() method? – Johnathan Au Nov 13 '14 at 14:25
  • 1
    You need to be able to implement the `CacheLoader`. To do this the all information needed to determine how to get the value must be obtainable from the key passed to the cache and the `CacheLoader`. You can either wrap multiple values into your key (and implement equals / hashCode to just use your real key value) or determine it from the existing key. It looks like `fillCacheIfNecessary` determines the argument from the `orgId` that is passed. So the `CacheLoader` should be able to do the same thing. The CacheLoader can call other instance methods in your class for help. – John B Nov 13 '14 at 14:47
  • 1
    Update made in answer – John B Nov 13 '14 at 17:20
  • Just to confirm, I don't even need the updateCacheIfNecessary() method at all then? – Johnathan Au Nov 13 '14 at 17:32
  • 1
    correct. That is the point of using a LoadingCache. It calls the CacheLoader for you as needed to populate the cache. – John B Nov 13 '14 at 17:45
  • Thank you. This all worked pretty good. LoadingCache and CacheLoader were indeed much simpler to use. – Johnathan Au Nov 14 '14 at 22:01