3

I have a list of jobs that need to be cached (by id). In some cases however, it is important to have the latest version of a job, and the cache needs to be bypassed (force update). When that happens, the newly fetched job should be placed in the cache.

I implemented it like this:

@Cacheable(cacheNames = "jobs", key = "#id", condition = "!#forceRefresh", sync = true)
public Job getJob(String id, boolean forceRefresh) {
    // expensive fetch
}

Desired behavior:

  • getJob("123", false) => job v1 is returned (fetched from the cache if present)
  • getJob("123", true) => job v2 is returned (updated version, fetched from db)
  • getJob("123", false) => job v2 is returned (updated version, fetched from cache)

In reality, the last call getJob("123", false) returns job v1, the stale version. It seems like the second call (forced update) does not update the value in the cache.

How can I achieve the correct behavior here?

Cache config (using Caffeine):

CaffeineCache jobs = new CaffeineCache("jobs", Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .maximumSize(100)
        .build());
fikkatra
  • 5,605
  • 4
  • 40
  • 66

2 Answers2

5

In case forceRefresh is true, Spring cache won't be activated because of the condition condition = "!#forceRefresh". Hence, the cache value won't be updated.

You need to explicitly tell Spring to update the cache value with @CachePut in case forceRefresh is true:

@Caching(
    cacheable = {@Cacheable(cacheNames = "jobs", key = "#id", condition = "!#forceRefresh")},
    put = {@CachePut(cacheNames = "jobs", key = "#id", condition = "#forceRefresh")}
)
public Job getJob(String id, boolean forceRefresh) {
    // expensive fetch
}
fikkatra
  • 5,605
  • 4
  • 40
  • 66
  • https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/html/cache.html#cache-annotations-put -- *Note that using CachePut and Cacheable annotations on the same method is generally strongly discouraged...* – Madbreaks Jan 22 '20 at 00:17
  • https://docs.spring.io/spring-framework/docs/4.3.17.RELEASE/spring-framework-reference/html/cache.html#cache-annotations-put _"... with the exception of specific corner-cases (such as annotations having conditions that exclude them from each other)."_ – Zeph Jun 29 '23 at 20:05
2

I have run into this problem before and solved it a couple of ways. The easiest way to solve it is to all your updates to Job done through this same JobService you are using. If that is the case you just do this:

    @Caching(evict = {
            @CacheEvict(value = "jobs", key = "#job.id") })
    public void updateJob( Job job ) {

This way when the Job is updated it will be evicted in the cache and your next call to getJob will pull a fresh one.

The next way is if you have some other process updating your database and updateJob is not used to update the actual source. At that point, I have implemented it where I built a Quartz Job to refresh/update my cache entry on a schedule (i.e. every 15 mins). It looks something like this.

    @Autowired
    CacheManager cacheManager;

    public void refreshJobs() {
        Cache cache = cacheManager.getCache( "jobs" );

        for ( Job job : getJobs() ) {
            cache.put( job.getId(), job );
        }
    }

You might get some stale Jobs with that solution, but you know it is being refreshed every 5, 10 or 15 mins.

Chris Savory
  • 2,597
  • 1
  • 17
  • 27
  • Thank you for your answer. Unfortunately, updates are done by some process I don't have control of, so the first solution is not an option for me. As for the second solution: I really need the latest version of the job when forceRefresh is true, so that won't work either. – fikkatra Dec 30 '19 at 07:40
  • How do you know when to call with forceRefresh of true? – Chris Savory Jan 01 '20 at 18:08
  • That's a client decision. In my case, I get the parameter from the front end. – fikkatra Jan 03 '20 at 07:39