Search code examples
javaspringcachinggemfirespring-data-gemfire

Gemfire EntryNotFoundException for @CacheEvict


In short, when @CacheEvict is called on a method and if the key for the entry is not found, Gemfire is throwing EntryNotFoundException.

Now in detail,

I have a class

class Person {

 String mobile;
 int dept;
 String name;

}

I have two Cache regions defined as personRegion and personByDeptRegion and the Service is as below

@Service
class PersonServiceImpl {

   @Cacheable(value = "personRegion")
   public Person findByMobile(String mobile) {

      return personRepository.findByMobile(mobile);

   }


   @Cacheable(value = "personByDeptRegion")
   public List<Person> findByDept(int deptCode) {

      return personRepository.findByDept(deptCode);

   }


   @Caching(
      evict = { @CacheEvict(value = "personByDeptRegion", key="#p0.dept"},
      put = { @CachePut(value = "personRegion",key = "#p0.mobile")}

   )
   public Person updatePerson(Person p1) {

      return personRepository.save(p1);

   }

}

When there is a call to updatePerson and if there are no entries in the personByDeptRegion, this would throw an exception that EntryNotFoundException for the key 1 ( or whatever is the dept code ). There is a very good chance that this method will be called before the @Cacheable methods are called and want to avoid this exception. Is there any way we could tweak the Gemfire behavior to gracefully return when the key is not existing for a given region ?. Alternatively, I am also eager to know if there is a better implementation of the above scenario using Gemfire as cache.

Spring Data Gemfire : 1.7.4

Gemfire Version : v8.2.1

Note: The above code is for representation purpose only and I have multiple services with same issue in actual project.


Solution

  • First, I commend you for using Spring's Caching annotations on your application @Service components. All too often developers enable caching in their Repositories, which I think is bad form, especially if complex business rules (or even additional IO; e.g. calling a web service from a service component) are involved prior to or after the Repository interaction(s), particularly in cases where caching behavior should not be affected (or determined).

    I also think your caching UC (updating one cache (personRegion) while invalidating another (personByDeptRegion) on a data store update) by following a CachePut with a CacheEvict seems reasonable to me. Though, I would point out that the seemingly intended use of the @Caching annotation is to combine multiple Caching annotations of the same type (e.g. multiple @CacheEvict or multiple @CachePut) as explained in the core Spring Framework Reference Guide. Still, there is nothing preventing your intended use.

    I created a similar test class here, modeled after your example above, to verify the problem. Indeed the jonDoeUpdateSuccessful test case fails (with the GemFire EntryNotFoundException, shown below) since no people in Department "R&D" were previously cached in the "DepartmentPeople" GemFire Region prior to the update, unlike the janeDoeUpdateSuccessful test case, which causes the cache to be populated before the update (even if the entry has no values, which is of no consequence).

    com.gemstone.gemfire.cache.EntryNotFoundException: RESEARCH_DEVELOPMENT
        at com.gemstone.gemfire.internal.cache.AbstractRegionMap.destroy(AbstractRegionMap.java:1435)
    

    NOTE: My test uses GemFire as both a "cache provider" and a System of Record (SOR).

    The problem really lies in SDG's use of Region.destroy(key) in the GemfireCache.evict(key) implementation rather than, and perhaps more appropriately, Region.remove(key).

    GemfireCache.evict(key) has been implemented with Region.destroy(key) since inception. However, Region.remove(key) was not introduced until GemFire v5.0. Still, I can see no discernible difference between Region.destroy(key) and Region.remove(key) other than the EntryNotFoundException thrown by Region.destroy(key). Essentially, they both destroy the local entry (both key and value) as well as distribute the operation to other caches in the cluster (providing a non-LOCAL Scope is used).

    So, I have filed SGF-539 to change SDG to call Region.remove(key) in GemfireCache.evict(key) rather than Region.destroy(key).

    As for a workaround, well, there is basically only 2 things you can do:

    1. Restructure your code and your use of the @CacheEvict annotation, and/or...
    2. Make use of the condition on @CacheEvict.

    It is unfortunate that a condition cannot be specified using a class type, something akin to a Spring Condition (in addition to SpEL), but this interface is intended for another purpose and the @CacheEvict, condition attribute does not accept a class type.

    At the moment, I don't have a good example of how this might work so I am moving forward on SGF-539.

    You can following this ticket for more details and progress.

    Sorry for the inconvenience.

    -John