9

I am using Spring Boot 1.5.9 on Tomcat 9.0.2 and I am trying to cache lookups using spring @Cacheable scheduling a cache refresh job that runs on application startup and repeats every 24 hours as follows:

@Component
public class RefreshCacheJob {

    private static final Logger logger = LoggerFactory.getLogger(RefreshCacheJob.class);

    @Autowired
    private CacheService cacheService;

    @Scheduled(fixedRate = 3600000 * 24, initialDelay = 0)
    public void refreshCache() {
        try {
            cacheService.refreshAllCaches();
        } catch (Exception e) {
            logger.error("Exception in RefreshCacheJob", e);
        }
    }

}

and the cache service is as follows:

@Service
public class CacheService {

    private static final Logger logger = LoggerFactory.getLogger(CacheService.class);

    @Autowired
    private CouponTypeRepository couponTypeRepository;

    @CacheEvict(cacheNames = Constants.CACHE_NAME_COUPONS_TYPES, allEntries = true)
    public void clearCouponsTypesCache() {}

    public void refreshAllCaches() {
        clearCouponsTypesCache();
        List<CouponType> couponTypeList = couponTypeRepository.getCoupons();
        logger.info("######### couponTypeList: " + couponTypeList.size());
    }
}

the repository code:

public interface CouponTypeRepository extends JpaRepository<CouponType, BigInteger> {
    @Query("from CouponType where active=true and expiryDate > CURRENT_DATE order by priority")
    @Cacheable(cacheNames = Constants.CACHE_NAME_COUPONS_TYPES)
    List<CouponType> getCoupons();
}

later in my webservice, when trying to get the lookup as follows:

@GET
@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
@Path("/getCoupons")
@ApiOperation(value = "")
public ServiceResponse getCoupons(@HeaderParam("token") String token, @HeaderParam("lang") String lang) throws Exception {
    try {
        List<CouponType> couponsList = couponRepository.getCoupons();
        logger.info("###### couponsList: " + couponsList.size());
        return new ServiceResponse(ErrorCodeEnum.SUCCESS_CODE, resultList, errorCodeRepository, lang);
    } catch (Exception e) {
        logger.error("Exception in getCoupons webservice: ", e);
        return new ServiceResponse(ErrorCodeEnum.SYSTEM_ERROR_CODE, errorCodeRepository, lang);
    }
}

The first call it gets the lookup from the database and the subsequent calls it gets it from the cache, while it should get it from the cache in the first call in the web service?

Why am I having this behavior, and how can I fix it?

halfer
  • 19,824
  • 17
  • 99
  • 186
Mahmoud Saleh
  • 33,303
  • 119
  • 337
  • 498
  • the caching annotation creates a proxy around the service. `refreshAllCaches` however bypasses this proxy and calls the method directly within the service. the solution is to call `clearCouponsTypesCache` from a different service – msp Feb 02 '18 at 10:36
  • Is `@EnableScheduling` present on the application config class? – jihor Feb 04 '18 at 12:00
  • yes it's enabled – Mahmoud Saleh Feb 04 '18 at 17:25

3 Answers3

2

The issue was fixed after upgrading to Tomcat 9.0.4

Mahmoud Saleh
  • 33,303
  • 119
  • 337
  • 498
1

While it's not affecting the scheduled task per se, when refreshAllCaches() is invoked in the CacheService, @CacheEvict on clearCouponsTypesCache() is bypassed since it's invoked from the same class (see this answer). It will lead to cache not being purged before

List<CouponType> couponTypeList = couponTypeRepository.getCoupons();

is invoked. This means that the @Cacheable getCoupons() method will not query the database, but will instead return values from the cache.

This makes the scheduled cache refresh action to do its work properly only once, when the cache is empty. After that it's useless.

The @CacheEvict annotation should be moved to refreshAllCaches() method and add beforeInvocation=true parameter to it, so the cache is purged before being populated, not after.


Also, when using Spring 4 / Spring Boot 1.X, these bugs should be taken into consideration:

While this bug doesn't seem to affect this specific program, it might be a good idea to separate @Cacheable annotation from JpaRepository interface until migration to Spring 5 / Spring Boot 2.X.

jihor
  • 2,478
  • 14
  • 28
0

@CacheEvict won't be invoked when called within the same service. This is because Spring creates a proxy around the service and only calls from "outside" go through the cache proxy.

The solution is to either add

@CacheEvict(cacheNames = Constants.CACHE_NAME_COUPONS_TYPES, allEntries = true)

to refreshAllCaches too, or to move refreshAllCaches into a new service that calls ICacheService.clearCouponsTypeCache.

msp
  • 3,272
  • 7
  • 37
  • 49