package com.vmware.vhadoop.vhm; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.vmware.vhadoop.api.vhm.HadoopActions.HadoopClusterInfo; import com.vmware.vhadoop.api.vhm.events.ClusterStateChangeEvent.SerengetiClusterConstantData; import com.vmware.vhadoop.api.vhm.events.ClusterStateChangeEvent.VMConstantData; import com.vmware.vhadoop.api.vhm.events.ClusterStateChangeEvent.VMVariableData; @SuppressWarnings("unchecked") /* This is a version of ClusterMap that caches results from public methods * * Not every ClusterMap method is implemented in this class as some are trivial and not worth caching * Note also that this caching makes the immutability of the objects returned by ClusterMap absolutely critical * * The caching works as follows: * AbstractClusterMap has two maps: One with information about clusters (ClusterInfo map) and one with information about vms (VMInfo map) * The public methods implemented here will access one or both of these maps and other than input parameters to the method, * this is their only other input. * Therefore, any time a change is made to one of these maps or an entry within it, all of the cached data for methods that use that map * as input data should be flushed. Changes to the maps are tracked using the VMCollectionUpdateListener and ClusterCollectionUpdateListener * Changes to entries within the maps are tracked using VMUpdateListener and ClusterUpdateListener. * Note ClusterMap code MUST use get/set methods to modify the state of ClusterInfo and VMInfo objects in order for the tracking to work * so these types are declared in the ClusterMap interface to prevent direct access to internal fields. * Most methods access either the VMInfo map or ClusterInfo map, but some methods access both. To avoid unnecessary flushing, * there are therefore 3 cache maps that correspond to these 3 types of access pattern. * The caches use the method name and its input parameters as the key and the object returned as the value * The caching works by looking first in the appropriate map. If a cached entry is not found, the equivalent method in BaseClusterMap * is invoked using reflection. The method has the same name as the calling method, with a postfix tag to identify it. * Since each method needs to know which of the 3 cache maps to use, this should not be left up to trial and error. As such, there are * checks performed every time a non-cached method is invoked to ensure that the correct cache map is being used. This is done by tracking * the number of times a VMInfo or ClusterInfo map is accessed during the invocation of the method. * The cache maps are synchronized because ClusterMap has a concurrent read model */ public class CachingClusterMapImpl extends BaseClusterMap { private static final Logger _log = Logger.getLogger(CachingClusterMapImpl.class.getName()); /* The 3 cache maps */ private Map<InputParams, Object> _resultFromVmList = Collections.synchronizedMap(new HashMap<InputParams, Object>()); private Map<InputParams, Object> _resultFromClusterList = Collections.synchronizedMap(new HashMap<InputParams, Object>()); private Map<InputParams, Object> _resultFromVmAndClusterList = Collections.synchronizedMap(new HashMap<InputParams, Object>()); /* Methods are cached to avoid multiple reflection lookups */ private Map<String, Method> _methods = Collections.synchronizedMap(new HashMap<String, Method>()); @Override public Set<String> listComputeVMsForCluster(String clusterId) { /* The MethodAccessor is a simple trick to allow us to use class.getEnclosingMethod() to avoid hard-coding method names */ class MethodAccessor {}; return (Set<String>)getCachedObjectFromVmList(MethodAccessor.class, clusterId); } @Override public Set<String> listComputeVMsForClusterAndPowerState(String clusterId, boolean powerState) { class MethodAccessor {}; return (Set<String>)getCachedObjectFromVmList(MethodAccessor.class, clusterId, powerState); } @Override public Set<String> listComputeVMsForClusterHostAndPowerState(String clusterId, String hostId, boolean powerState) { class MethodAccessor {}; return (Set<String>)getCachedObjectFromVmList(MethodAccessor.class, clusterId, hostId, powerState); } @Override public Set<String> listComputeVMsForPowerState(boolean powerState) { class MethodAccessor {}; return (Set<String>)getCachedObjectFromVmList(MethodAccessor.class, powerState); } @Override public Set<String> listHostsWithComputeVMsForCluster(String clusterId) { class MethodAccessor {}; return (Set<String>)getCachedObjectFromVmList(MethodAccessor.class, clusterId); } @Override public Map<String, String> getHostIdsForVMs(Set<String> vmsToED) { class MethodAccessor {}; return (Map<String, String>)getCachedObjectFromVmList(MethodAccessor.class, vmsToED); } @Override public Boolean checkPowerStateOfVms(Set<String> vmIds, boolean expectedPowerState) { class MethodAccessor {}; return (Boolean)getCachedObjectFromVmList(MethodAccessor.class, vmIds, expectedPowerState); } @Override public Boolean checkPowerStateOfVm(String vmId, boolean expectedPowerState) { class MethodAccessor {}; return (Boolean)getCachedObjectFromVmList(MethodAccessor.class, vmId, expectedPowerState); } @Override public Map<String, String> getDnsNamesForVMs(Set<String> vmIds) { class MethodAccessor {}; return (Map<String, String>)getCachedObjectFromVmList(MethodAccessor.class, vmIds); } @Override public String getDnsNameForVM(String vmId) { class MethodAccessor {}; return (String)getCachedObjectFromVmList(MethodAccessor.class, vmId); } @Override public Map<String, String> getVmIdsForDnsNames(Set<String> dnsNames) { class MethodAccessor {}; return (Map<String, String>)getCachedObjectFromVmList(MethodAccessor.class, dnsNames); } @Override public String getVmIdForDnsName(String dnsName) { class MethodAccessor {}; return (String)getCachedObjectFromVmList(MethodAccessor.class, dnsName); } @Override public String getClusterIdForName(String clusterFolderName) { class MethodAccessor {}; return (String)getCachedObjectFromClusterList(MethodAccessor.class, clusterFolderName); } @Override public Set<String> getAllClusterIdsForScaleStrategyKey(String key) { class MethodAccessor {}; return (Set<String>)getCachedObjectFromVmAndClusterList(MethodAccessor.class, key); } @Override public String getScaleStrategyKey(String clusterId) { class MethodAccessor {}; return (String)getCachedObjectFromVmAndClusterList(MethodAccessor.class, clusterId); } @Override public HadoopClusterInfo getHadoopInfoForCluster(String clusterId) { class MethodAccessor {}; return (HadoopClusterInfo)getCachedObjectFromVmAndClusterList(MethodAccessor.class, clusterId); } @Override public String getMasterVmIdForCluster(String clusterId) { class MethodAccessor {}; return (String)getCachedObjectFromClusterList(MethodAccessor.class, clusterId); } @Override public Map<String, Set<String>> getNicAndIpAddressesForVm(String vmId) { class MethodAccessor {}; return (Map<String, Set<String>>)getCachedObjectFromVmList(MethodAccessor.class, vmId); } @Override /* We override this method so that we can add the VMUpdateListener to each VMInfo */ VMInfo createVMInfo(String moRef, VMConstantData constantData, VMVariableData variableData, String clusterId) { VMInfo result = super.createVMInfo(moRef, constantData, variableData, clusterId); result.setUpdateListener(new VMUpdateListener() { @Override public void updatingVM(String moRef) { resetVMCache(false); } }); return result; } @Override /* We override this method so that we can add the ClusterUpdateListener to each ClusterInfo */ ClusterInfo createClusterInfo(String clusterId, SerengetiClusterConstantData constantData) { ClusterInfo result = super.createClusterInfo(clusterId, constantData); result.setUpdateListener(new ClusterUpdateListener() { @Override public void updatingCluster(String clusterId) { resetClusterCache(false); } }); return result; } /* Simple key type used for indexing cache entries */ private class InputParams { List<Object> _params = new ArrayList<Object>(); String _methodName; InputParams(String methodName, Object... objects) { _params = Arrays.asList(objects); _methodName = methodName; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + ((_methodName == null) ? 0 : _methodName.hashCode()); result = prime * result + ((_params == null) ? 0 : _params.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; InputParams other = (InputParams) obj; if (!getOuterType().equals(other.getOuterType())) return false; if (_methodName == null) { if (other._methodName != null) return false; } else if (!_methodName.equals(other._methodName)) return false; if (_params == null) { if (other._params != null) return false; } else if (!_params.equals(other._params)) return false; return true; } private CachingClusterMapImpl getOuterType() { return CachingClusterMapImpl.this; } } /* Here's where we add the VMCollectionUpdateListener and ClusterCollectionUpdateListener */ public CachingClusterMapImpl(ExtraInfoToClusterMapper mapper) { super(mapper); setVMCollectionUpdateListener(new VMCollectionUpdateListener() { @Override public void updatingVMCollection() { resetVMCache(true); } }); setClusterCollectionUpdateListener(new ClusterCollectionUpdateListener() { @Override public void updatingClusterCollection() { resetClusterCache(true); } }); } private void resetVMCache(boolean updatingCollection) { _log.finer("Resetting VM cache"); _resultFromVmList.clear(); _resultFromVmAndClusterList.clear(); } private void resetClusterCache(boolean updatingCollection) { _log.finer("Resetting Cluster cache"); _resultFromClusterList.clear(); _resultFromVmAndClusterList.clear(); } /* Either return the cached result, or invoke the Base method from the superclass */ private Object getCachedObjectFromList(Map<InputParams, Object> inputMap, Class<?> methodAccessor, Object... args) { String methodName = methodAccessor.getEnclosingMethod().getName(); InputParams params = new InputParams(methodAccessor.getEnclosingMethod().getName(), args); Object result = inputMap.get(params); if (result == null) { try { Method method = getSuperclassMethod(methodAccessor); result = method.invoke(this, args); } catch (Exception e) { _log.log(Level.SEVERE, "VHM: unexpected exception invoking cached method - " + e.getMessage()); _log.log(Level.INFO, "Unexpected exception invoking cached method", e); } if (result != null) { _log.finer("Caching new result from "+methodName+"; hashcode="+result.hashCode()+", identity hashcode="+System.identityHashCode(result)); inputMap.put(params, result); } } else { _log.finer("Returning cached result from "+methodName+"; hashcode="+result.hashCode()+", identity hashcode="+System.identityHashCode(result)); } return result; } private Object getCachedObjectFromVmList(Class<?> methodAccessor, Object... args) { DataCheck dataCheck = _dataCheckMap.get(Thread.currentThread().getName()); int clusterAccessCount = 0; if (dataCheck != null) { clusterAccessCount = dataCheck._clusterInfoMapAccessCount; } Object result = getCachedObjectFromList(_resultFromVmList, methodAccessor, args); /* Double-check that a method purporting to only access the VMInfo map doesn't read the ClusterInfo map */ if ((dataCheck != null) && (clusterAccessCount != dataCheck._clusterInfoMapAccessCount)) { _log.severe("Cached method claiming to only access vm data attempted to access ClusterInfo map!"); } return result; } private Object getCachedObjectFromClusterList(Class<?> methodAccessor, Object... args) { DataCheck dataCheck = _dataCheckMap.get(Thread.currentThread().getName()); int vmAccessCount = 0; if (dataCheck != null) { vmAccessCount = dataCheck._vmInfoMapAccessCount; } Object result = getCachedObjectFromList(_resultFromClusterList, methodAccessor, args); /* Double-check that a method purporting to only access the ClusterInfo map doesn't access the VMInfo map */ if ((dataCheck != null) && (vmAccessCount != dataCheck._vmInfoMapAccessCount)) { _log.severe("Cached method claiming to only access cluster data attempted to access VMInfo map!"); } return result; } /* Objects that access vm and cluster data have their own cache which is cleared whenever either changes */ private Object getCachedObjectFromVmAndClusterList(Class<?> methodAccessor, Object... args) { return getCachedObjectFromList(_resultFromVmAndClusterList, methodAccessor, args); } /* Caches a Method object representing the base method in the superclass, indexed by method signature */ private Method getSuperclassMethod(Class<?> methodAccessor) { Method method = methodAccessor.getEnclosingMethod(); Method result = _methods.get(method.toString()); if (result == null) { try { Class<?>[] argsList = methodAccessor.getEnclosingMethod().getParameterTypes(); result = getClass().getSuperclass().getDeclaredMethod(method.getName()+BASE_METHOD_POSTIFX, argsList); result.setAccessible(true); _methods.put(method.toString(), result); } catch (Exception e) { _log.log(Level.SEVERE, "VHM: unexpected exception getting cached method - " + e.getMessage()); _log.log(Level.INFO, "Unexpected exception getting cached method", e); } } return result; } }