I have the following method in one of my services. Since the operation in getHierarchy()
method is expensive, I chose to cache it:
@Service
@RequiredArgsConstructor
@Log4j2
public class ProductHierarchyServiceImpl implements ProductHierarchyService {
private final ProductGroupRepository repository;
private final ProductTreeMapper mapper;
@Override
@Cacheable(value = HIERARCHY)
@Trace(dispatcher = true)
public List<ProductHierarchyBean> getHierarchy() {
var productGroups = repository.findAll();
return mapper.productGroupsToProductHierarchyBeans(productGroups);
}
That works well and I confirm the List<ProductHierarchyBean>
gets cached between cache purges so the method is not invoked, as expected.
I then invoke that method from another service:
@Service
@RequiredArgsConstructor
@Log4j2
public class ProductGroupAccessServiceImpl implements ProductGroupAccessService {
private final LicenceCategoryAccessRepository accessRepository;
private final ProductHierarchyService productHierarchyService;
@Override
public List<ProductHierarchyBean> getFullHierarchy(Integer licenceId) {
List<ProductHierarchyBean> hierarchy = productHierarchyService.getHierarchy();
... // more transformations here to add licensing to the hierarchy nodes
return hierarchy;
}
@Override
public List<ProductHierarchyBean> getLicensedHierarchy(Integer licenceId) {
List<ProductHierarchyBean> licensedHierarchy = getFullHierarchy(licenceId);
trimHierarchyToLicencedNodes(licensedHierarchy); ... // perform transformations here to remove unlicensed nodes
return licensedHierarchy;
}
The idea is the ProductHierarchyService
provides full hierarchy of all available products, whereas the ProductGroupAccessService
applies licence-based filtering that removes non-licensed nodes.
Both services methods of ProductGroupAccessService
are invoked from different end points, getFullHierarchy()
to retrieve full hierarchy used for editing hierarchy by admin users, and getLicensedHierarchy()
by the actual user who builds a report based on the licensed hierarchy (and only have access to the allowed nodes).
So far so good, the problem starts when these REST end points (or the above methods directly) are invoked in turn. If getFullHierarchy
invoked after getLicensedHierarchy
, the truncated hierarchy with unlicensed nodes removed is served as if it were the full hierarchy from the cache.
If I remove the @Cache
from ProductHierarchyService.getHierarchy()
it all works as expected, and a hierarchy gets refreshed every time, albeit at a speed cost.
I figured the downstream service uses the same object as initially was retrieved and stored in the cache, and it modifies that.
To overcome that, I've implemented a cloneHierarchy()
method, that creates a fresh copy of the hiserarchy, which I use as below:
@Override
@Cacheable(value = ARGOSHIERARCHY)
@Trace(dispatcher = true)
private List<ProductHierarchyBean> getHierarchy() {
var productGroups = repository.findAll();
var root = mapper.productGroupsToProductHierarchyBeans(productGroups);
return cloneHierarchy(root);
}
But that still doesn't work since I clone inside the @Cached method itself, so whatever is returned last gets cached.
My cache configuration is bog-standard, as below:
@Configuration
@EnableCaching
@EnableScheduling
@Log4j2
public class CacheConfiguration {
public static final String HIERARCHY = "hierarchy";
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(
List.of(
new ConcurrentMapCache(HIERARCHY));
return cacheManager;
}
@CacheEvict(
allEntries = true,
value = {HIERARCHY})
@Scheduled(fixedDelayString = "${products.hierarchy.cacheMsDelay:60000}")
public void reportCacheEvict() {
log.debug("Flush Cache {}", LocalDateTime.now());
}
}
Any suggestions how can I overcome this problem please? Basically, I want the hierarchy to be cached, but only one single copy, that I can clone and pass to a client on demand?
Figured it out - apparently I needed to clone the hierarchy in the upstream method, like this:
@Override
public List<ProductHierarchyBean> getFullHierarchy(Integer licenceId) {
List<ProductHierarchyBean> hierarchy = productHierarchyService.getHierarchy();
hierarchy = ProductHierarchyCloner.cloneHierarchy(hierarchy);
...
}
This way the inital full hierarchy stays cached and we clone it every time we need to use it.