package rocks.inspectit.agent.java.analyzer.impl; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.collections.MapUtils; import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import rocks.inspectit.agent.java.config.IConfigurationStorage; import rocks.inspectit.agent.java.io.FileResolver; import rocks.inspectit.agent.java.spring.PrototypesProvider; import rocks.inspectit.shared.all.instrumentation.config.impl.InstrumentationDefinition; import rocks.inspectit.shared.all.serializer.impl.SerializationManager; import rocks.inspectit.shared.all.serializer.provider.SerializationManagerProvider; import rocks.inspectit.shared.all.spring.logger.Log; /** * Implementation of the {@link IClassHashHelper} that holds all data in one concurrent map. Keys in * this map are class FQNs, while entries are {@link ClassEntry}s and they define answers to all the * provided questions. * * @author Ivan Senic * */ // we must depend on PlatformManager to make sure that initial instrumentations are in the // configuration @Component @DependsOn("platformManager") public class ClassHashHelper implements InitializingBean, DisposableBean { /** * Logger for the class. */ @Log Logger log; /** * Configuration storage to read the configuration properties. */ @Autowired private IConfigurationStorage configurationStorage; /** * Core-service executor service. */ @Autowired @Qualifier("coreServiceExecutorService") private ScheduledExecutorService executorService; /** * {@link SerializationManagerProvider} for the serialization. */ @Autowired private PrototypesProvider prototypesProvider; /** * {@link FileResolver}. */ @Autowired private FileResolver fileResolver; /** * Serialization manager to use when storing loading from disk. */ private SerializationManager serializationManager; /** * Map holding class entries. Key is FQN of the class. * <p> * Initial capacity to 4K, as we don't really expect less amount of classes in average. */ private final ConcurrentHashMap<String, ClassEntry> fqnToClassEntryMap = new ConcurrentHashMap<String, ClassEntry>(4096); /** * Registers that the given class was analyzed. * * @param fqn * Class fqn * */ public void registerAnalyzed(String fqn) { getOrCreateEntry(fqn); } /** * Returns if the class with given FQN was analyzed. * * @param fqn * Class fqn * * @return Returns if the class with given FQN was analyzed. */ public boolean isAnalyzed(String fqn) { ClassEntry entry = fqnToClassEntryMap.get(fqn); return entry != null ? true : false; } /** * Registers the class with given fqn/hash as being sent to the CMR. * * @param fqn * Class fully qualified name. * @param hash * Class hash */ public void registerSent(String fqn, String hash) { ClassEntry entry = getOrCreateEntry(fqn); entry.addHash(hash); } /** * Returns if the class with given fqn and hash has been sent to the CMR. Only hashes that are * registered with {@link #registerSent(String)} are considered as sent ones. * * @param fqn * Class fully qualified name. * @param hash * Hash to check * * * @return Returns if the class with given hash has been sent to the CMR. */ public boolean isSent(String fqn, String hash) { ClassEntry entry = fqnToClassEntryMap.get(fqn); return entry != null ? entry.containsHash(hash) : false; } /** * Registers the instrumentation result for the class with the given FQn. * * @param fqn * Class fully qualified name. * @param instrumentationResult * {@link InstrumentationDefinition} */ public void registerInstrumentationDefinition(String fqn, InstrumentationDefinition instrumentationResult) { ClassEntry entry = getOrCreateEntry(fqn); if ((null != instrumentationResult) && !instrumentationResult.isEmpty()) { entry.setInstrumentationResult(instrumentationResult); } else { entry.setInstrumentationResult(null); // NOPMD } } /** * Returns the {@link InstrumentationDefinition} for the class with given FQN if the one was * been set with the * {@link #registerInstrumentationDefinition(String, InstrumentationDefinition)}. * * @param fqn * Class fqn * @return {@link InstrumentationDefinition} or <code>null</code> if no result was set for given * hash. */ public InstrumentationDefinition getInstrumentationDefinition(String fqn) { ClassEntry entry = fqnToClassEntryMap.get(fqn); return entry != null ? entry.getInstrumentationResult() : null; } /** * Creates new entry in the map in the atomic fashion. * * @param fqn * key * @return Created or existing entry */ private ClassEntry getOrCreateEntry(String fqn) { ClassEntry entry = fqnToClassEntryMap.get(fqn); if (null == entry) { entry = new ClassEntry(); ClassEntry old = fqnToClassEntryMap.putIfAbsent(fqn, entry); if (null != old) { entry = old; } } return entry; } /** * Returns if no class has been cached with this helper. * * @return Returns if no class has been cached with this helper. */ boolean isEmpty() { return fqnToClassEntryMap.isEmpty(); } /** * {@inheritDoc} * <P> * Loads the possible existing class cache from the disk if CMR reports to know classes from * this agent. */ @Override public void afterPropertiesSet() throws Exception { serializationManager = prototypesProvider.createSerializer(); // only load if configuration says that the class cache exists on the CMR if (configurationStorage.isClassCacheExistsOnCmr()) { loadCacheFromDisk(); } else { deleteCacheFromDisk(); } // check if there are any initial instrumentation points in configuration Map<Collection<String>, InstrumentationDefinition> initInstrumentations = configurationStorage.getInitialInstrumentationResults(); if (MapUtils.isNotEmpty(initInstrumentations)) { for (Entry<Collection<String>, InstrumentationDefinition> entry : initInstrumentations.entrySet()) { InstrumentationDefinition instrumentationResult = entry.getValue(); String fqn = instrumentationResult.getClassName(); registerInstrumentationDefinition(fqn, instrumentationResult); for (String hash : entry.getKey()) { registerSent(fqn, hash); } } } Runnable saveCacheToDiskRunnable = new Runnable() { @Override public void run() { saveCacheToDisk(); } }; executorService.scheduleAtFixedRate(saveCacheToDiskRunnable, 30, 300, TimeUnit.SECONDS); } /** * {@inheritDoc} */ @Override public void destroy() throws Exception { // save when bean is destroyed, ensure save is always done on finishing saveCacheToDisk(); fqnToClassEntryMap.clear(); } /** * Load sent classes from disk. */ @SuppressWarnings("unchecked") private void loadCacheFromDisk() { File file = fileResolver.getClassHashCacheFile().getAbsoluteFile(); if (file.exists()) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(file); Input input = new Input(fileInputStream); Map<String, Collection<String>> fqnWithHashes = (Map<String, Collection<String>>) serializationManager.deserialize(input); for (Entry<String, Collection<String>> entry : fqnWithHashes.entrySet()) { ClassEntry classEntry = getOrCreateEntry(entry.getKey()); for (String hash : entry.getValue()) { classEntry.addHash(hash); } } } catch (Throwable t) { // NOPMD log.warn("Unable to load sending classes cache from disk.", t); } finally { if (null != fileInputStream) { try { fileInputStream.close(); } catch (IOException e) { // NOPMD //NOCHK // ignore } } } } } /** * Deletes the current cache file from disk. */ private void deleteCacheFromDisk() { File file = fileResolver.getClassHashCacheFile().getAbsoluteFile(); if (file.exists()) { if (!file.delete()) { log.warn("Unable to delete the existing class cache file: " + file.getAbsolutePath()); } } } /** * Save cache to disk. */ private void saveCacheToDisk() { File file = fileResolver.getClassHashCacheFile().getAbsoluteFile(); if (file.exists()) { if (!file.delete()) { log.warn("Unable to delete the existing class cache file: " + file.getAbsolutePath()); } } else { File parentDir = file.getParentFile(); if (!parentDir.exists()) { if (!parentDir.mkdirs()) { log.warn("Unable to create needed directory for the cache file: " + file.getParentFile().getAbsolutePath()); } } } FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(file); Output output = new Output(fileOutputStream); // save only the ones being set to the CMR Map<String, Collection<String>> fqnWithHashes = new HashMap<String, Collection<String>>(); for (Entry<String, ClassEntry> entry : fqnToClassEntryMap.entrySet()) { fqnWithHashes.put(entry.getKey(), entry.getValue().getHashes()); } serializationManager.serialize(fqnWithHashes, output); } catch (Throwable t) { // NOPMD log.warn("Unable to save sending classes cache to disk.", t); } finally { if (null != fileOutputStream) { try { fileOutputStream.close(); } catch (IOException e) { // NOPMD //NOCHK // ignore } } } } /** * Simple entry class that should hold the {@link InstrumentationDefinition} and collection of * class loaders for one class hash. * * @author Ivan Senic * */ private static class ClassEntry { /** * {@link InstrumentationDefinition}. <code>null</code> if one does not exist. */ private volatile InstrumentationDefinition instrumentationResult; /** * Known hashes for this class. */ private final CopyOnWriteArrayList<String> hashes = new CopyOnWriteArrayList<String>(); /** * Gets {@link #instrumentationResult}. * * @return {@link #instrumentationResult} */ public InstrumentationDefinition getInstrumentationResult() { return instrumentationResult; } /** * Sets {@link #instrumentationResult}. * * @param instrumentationResult * New value for {@link #instrumentationResult} */ public void setInstrumentationResult(InstrumentationDefinition instrumentationResult) { this.instrumentationResult = instrumentationResult; } /** * Adds hash to the {@link #hashes} if it does not exist. * * @param hash * of the class */ public void addHash(String hash) { if (null != hash) { hashes.addIfAbsent(hash); } } /** * Returns if the hash is contained in the {@link #hashes}. * * @param hash * of the class * @return Returns if the hash is contained in the {@link #hashes}. */ public boolean containsHash(String hash) { if (null != hash) { return hashes.contains(hash); } return false; } /** * Gets {@link #hashes}. * * @return {@link #hashes} */ public Collection<String> getHashes() { return Collections.unmodifiableList(hashes); } } }