ExpireAfterWrite does not seem to work

1.9k views Asked by At

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?

1

There are 1 answers

8
John B On BEST ANSWER

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.