I have a use-case where I have configured multiple cache managers with different properties, and different methods annotated with a separate cache name. The cached methods are retrieving data asynchronously from a http client, and caching the response. In the said use-case, the data from both the cached method is merged before returning the result. At times, the result contains data only from one of the cached methods, and on refreshing the issue is resolved. I am not able to understand in what instance the issue is raised?
@Configuraions
public class CacheConfig{
public static final String CACHE1 = "cache1";
public static final String CACHE2 = "cache2";
@Value("${cache.caffeineSpec:expireAfterWrite=43200s,maximumSize=1000,recordStats}")
private String cacheSpec1;
@Value("${cache.caffeineSpec: expireAfterWrite=3600s,maximumSize=2000,recordStats}")
private String cacheSpec2;
@Bean("cacheManager1")
@Primary
public CacheManager brokerDetailscacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager(CACHE1);
cacheManager.setCaffeine(Caffeine.from(cacheSpec1));
return cacheManager;
}
@Bean("cacheManager2")
public CacheManager brokerTierCodeMapCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager(CACHE2, BROKER_TIER_CACHE);
cacheManager.setCaffeine(Caffeine.from(cacheSpec2));
return cacheManager;
}
}
Models in use
public class Person {
private String firstname;
private String lastname;
private List<Address> adresses;
}
private class Address {
private String street;
private String City
private String zip;
}
private class PersonInfo {
private String firstname;
private String lastname;
private Address address;
}
The cached method classes are:
@Service
@RequiredArgsConstructor
public class PersonCache {
private final DataClient dataClient;
@Cacheable(cacheNames = CacheConfig.CACHE1, cacheManager = "cacheManager1" ,sync = true)
public Map<String, Person> getPersonDetails(String firstname) {
Map<String, Person> personMap = new HashMap()<>;
//Key is first name, grouping all results by firstname
try {
personMap = dataClient.getPersonDetails(firstname)
.toCompletableFuture()
.get(3, TimeUnit.SECONDS);
}catch(Exception e) {
log.error("Error fetching response from api". e);
}
}
@Cacheable(cacheNames = CacheConfig.CACHE2, cacheManager = "cacheManager2" ,sync = true)
public Map<String, Person> getPersonDetails(String firstname) {
List<PersonInfo> personMap = new ArrayList();
try {
personMap = dataClient.getPersonInfoDetails(firstname)
.toCompletableFuture()
.get(3, TimeUnit.SECONDS);
}catch(Exception e) {
log.error("Error fetching response from api". e);
}
return transformPersonInfoToPerson(personMap);
}
}
The calling method:
@Service
@RequiredArgsConstructor
public class PersonService {
private final PersonCache personCache;
public List<Person> getPersonDetails(String firstName) {
Map<String, Person> personResponse1 = personCache.getPersonDetails(firstName);
//.. after fetching for the first result set, check for a flag and call the below cache to append the data
Map<String, Person> personResponse2 = personCache.getPersonInfoDetails(firstName);
personResponse1.putAll(personResponse2);
// This when returned at times does not contain any response from personResponse1 and only contains the personResponse2 data
return personResponse1.values();
}
}
Is it possible that the asynchronous API calls are causing some sort of miss , and the result set of the second cache is added to the result and returned ? (The calling method is also called asynchronously from the controller class)
How should I handle to have the consistent response irrespective of the number of times the endpoint is triggered?
The cache key and value should be treated as immutable once they enter the cache. This is because they become available for multiple threads, so mutating an entry afterwards can become unpredictable. The behavior is less known when done in an unsafe way.
In your code the cached value is returned as the HashMap personResponse1
. It is then modified to include the entries of personResponse2
. At best this stores all of the contents in response1 for the next call, but it could also result in corruption as multiple threads write into it unsynchronized. When corrupted it may be that some entries cannot be found again, e.g. on resize they are not properly rehashed into the correct table location or are no longer on a bin's linked list. Another possibility is that since a mutable view of the values is returned to client code not shown, perhaps that code removes entries when processing it. The actual behavior becomes unpredictable, which is why it looks correct for a short time after refreshed because the cache discard the corrupted result.
A good practice would be to store an immutable Map.copy
or decorated with Collections.unmodifiableMap
. Then any mutations are disallowed and you would have caught this immediately. When consuming the cached responses merge them into a new map. Most likely your code was uncached so responses weren't stored and shared, but adding the cache here changed that so you now need to be mindful of the problems that arise with mutable shared state.