I have multiple questions related to HazelCast core as well as Spring Boot cache API.
Let me lay out the scenario first.
We have a monitoring system for monitoring multiple network infrastructures.
I have a Spring Boot app which could be deployed as multiple nodes behind a load-balancer. In addition to that, this same app can work for multiple infrastructures by just running it with different profile such as infra-1-prod, infra-2-prod etc.
Its horizontally scalable as well as versatile. This nature is achieved by running the application with different profiles.
Along with other things, this profile change, changes the underlying DB connections to a relational database which contains the configuration data for a particular infrastructure.
Have a look at the relevant architecture for the application
The same spring boot application could be run as a node for different infrastructures spawning its own HazelCast instance node. If we have 6 nodes for the application, there will be 6 nodes for the HazelCast cluster. All of them will be in sync.
Now I have a Repository named RuleRepository which returns the Rule data for a particular Rule Alias.
@Repository
public interface RuleRepository extends JpaRepository<Rule, Long> {
@Cacheable(value = Constants.CACHE_ALIAS)
Optional<Rule> findByAlias(String ruleAlias);
//some other functions
}
Now the problem is, as the Rule Aliases are auto generated by DB sequences, an alias R_123 points to different data for Infra-1 and Infra-2 nodes but because all the HazelCast nodes are in sync, incorrect data is overridden.
For this, I thought of giving different names to the cache for every infrastructure so that the cached data don't get jumbled.
Doing this is not straight forward because we can't inject properties into the cache names. For this we need to implement our own custom CacheResolver and CacheManager.
I will lay out my understanding of HazelCast before I ask the first question.
Every HazelCast Instance can have multiple Map Configurations which are basically just different caches. Every CacheManager can be linked with a Single HazelCast instance which will internally contain multiple caches.
Question 1: If the relationship between CacheManager and HazelCastInstance is one-to-one then how will I determine which method data will be cached into which cache (Map Config).
Here is the incomplete implementation I have with me currently
public class CacheableOperations {
private final CacheManager cacheManager;
private final CacheManager noOpCacheManager;
public CacheableOperations(CacheManager cacheManager, CacheManager noOpCacheManager) {
this.cacheManager = cacheManager;
this.noOpCacheManager = noOpCacheManager;
}
private Map<String, CacheableOperation<?>> opMap;
public void init() {
List<CacheableOperation<? extends Class>> ops = new ArrayList<>();
ops.add(new CacheableOperation.Builder(RuleRepository.class)
.method("findByAlias")
.cacheManager(cacheManager)
.build());
postProcessOperations(ops);
}
public CacheableOperation<?> get(CacheOperationInvocationContext<?> context) {
final String queryKey = getOperationKey(context.getTarget().getClass().getName(),
context.getMethod().getName());
return opMap.get(queryKey);
}
private void postProcessOperations(List<CacheableOperation<? extends Class>> ops) {
Map<String, CacheableOperation<?>> tempMap = new HashMap<>();
for (CacheableOperation<?> op : ops) {
for (String methodName : op.getMethodNames()) {
tempMap.put(getOperationKey(op.getTargetClass().getName(), methodName), op);
}
}
opMap = ImmutableMap.copyOf(tempMap);
}
private String getOperationKey(String first, String second) {
return String.format("%s-%s", first, second);
}
Here is the class for CacheConfiguration
@Configuration
@AllArgsConstructor
public class CacheConfiguration extends CachingConfigurerSupport {
private final CacheProperties cacheProperties;
private SysdiagProperties sysdiagProperties;
@Bean
@Override
public CacheManager cacheManager() {
return new HazelcastCacheManager(hazelcastInstance());
}
@Bean
@Profile("client")
HazelcastInstance hazelcastInstance() {
Config config = new Config();
config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(sysdiagProperties.getCache().getMemberIps()).setEnabled(true);
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
config.setInstanceName("restapi-master-cache-" + sysdiagProperties.getServiceName());
return Hazelcast.newHazelcastInstance(config);
}
@Bean
@Override
public CacheResolver cacheResolver() {
return new CustomCacheResolver(cacheProperties, operations(), noOpCacheManager());
}
@Bean
public CacheManager noOpCacheManager() {
return new NoOpCacheManager();
}
@Bean
public CacheableOperations operations() {
CacheableOperations operations = new CacheableOperations(cacheManager(), noOpCacheManager());
operations.init();
return operations;
}
And here is the CacheableOperation class
public class CacheableOperation<T> {
private final Class<T> targetClass;
private final String[] methodNames;
private final CacheManager cacheManager;
private CacheableOperation(Class<T> targetClass, String[] methodNames, CacheManager cacheManager) {
this.targetClass = targetClass;
this.methodNames = methodNames;
this.cacheManager = cacheManager;
}
public Class<T> getTargetClass() {
return targetClass;
}
public String[] getMethodNames() {
return methodNames;
}
public CacheManager getCacheManager() {
return cacheManager;
}
public static class Builder<T> {
private final Class<T> targetClass;
private String[] methodNames;
private CacheManager cacheManager;
private Map<String, Method> methods = new HashMap<>();
public Builder(Class<T> targetClass) {
this.targetClass = targetClass;
Arrays.stream(targetClass.getDeclaredMethods())
.forEachOrdered(method -> methods.put(method.getName(), method));
}
public Builder<T> method(String... methodNames) {
this.methodNames = methodNames;
return this;
}
public Builder<T> cacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
return this;
}
public CacheableOperation<T> build() {
checkArgument(targetClass != null);
checkArgument(ArrayUtils.isNotEmpty(methodNames));
checkArgument(Arrays.stream(methodNames).allMatch(name -> methods.get(name) != null));
return new CacheableOperation<T>(targetClass, methodNames, cacheManager);
}
}
}
And finally the CacheResolver
public class CustomCacheResolver implements CacheResolver {
private final CacheableOperations operations;
private final CacheProperties cacheProperties;
private final CacheManager noOpCacheManager;
public CustomCacheResolver(CacheProperties cacheProperties, CacheableOperations operations, CacheManager noOpCacheManager) {
this.cacheProperties = cacheProperties;
this.operations = operations;
this.noOpCacheManager = noOpCacheManager;
}
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
if (!cacheProperties.isEnabled()) {
return getCaches(noOpCacheManager, context);
}
Collection<Cache> caches = new ArrayList<>();
CacheableOperation operation = operations.get(context);
if (operation != null) {
CacheManager cacheManager = operation.getCacheManager();
if (cacheManager != null) {
caches = getCaches(cacheManager, context);
}
}
return caches;
}
private Collection<Cache> getCaches(CacheManager cacheManager, CacheOperationInvocationContext<?> context) {
return context.getOperation().getCacheNames().stream()
.map(cacheName -> cacheManager.getCache(cacheName))
.filter(cache -> cache != null)
.collect(Collectors.toList());
}
}
Question 2: In this whole code base, I cannot find the linkage between a Cache Name and a Method Name which I did in the first snippet. All I could see is a link between the method name and the
cacheManagerinstance. Where do I define that?
All the questions and documentation I read about Spring Boot and HazelCast, does not seem to go in great depth in this case.
Question 3: Can someone define the role of a
CacheResolverand aCacheManagerin a straight forward manner for me.
Thanks for the patience. Answer to even one of the question might help me a lot. :)
