/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.master.cache; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheException; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.config.CacheConfiguration; import net.sf.ehcache.config.CopyStrategyConfiguration; import net.sf.ehcache.config.SearchAttribute; import net.sf.ehcache.config.Searchable; import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; import net.sf.ehcache.search.Result; import net.sf.ehcache.search.Results; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Instant; import com.opengamma.DataNotFoundException; import com.opengamma.core.change.BasicChangeManager; import com.opengamma.core.change.ChangeEvent; import com.opengamma.core.change.ChangeListener; import com.opengamma.core.change.ChangeManager; import com.opengamma.id.ObjectId; import com.opengamma.id.ObjectIdentifiable; import com.opengamma.id.UniqueId; import com.opengamma.id.VersionCorrection; import com.opengamma.master.AbstractChangeProvidingMaster; import com.opengamma.master.AbstractDocument; import com.opengamma.util.ArgumentChecker; /** * A cache decorating a master, mainly intended to reduce the frequency and repetition of queries to the underlying * master. * <p> * The cache is implemented using {@code EHCache}. * * TODO Check whether misses are cached by SelfPopulatingCache * TODO remove redundant cleanCache calls * TODO externalise configuration in xml file * * * @param <D> the document type returned by the master */ public abstract class AbstractEHCachingMaster<D extends AbstractDocument> implements AbstractChangeProvidingMaster<D> { /** Logger. */ private static final Logger s_logger = LoggerFactory.getLogger(AbstractEHCachingMaster.class); /** Cache name. */ private static final String CACHE_NAME_SUFFIX = "UidToDocumentCache"; /** Check cached results against results from underlying */ public static final boolean TEST_AGAINST_UNDERLYING = false; //s_logger.isDebugEnabled(); /** The underlying master. */ private final AbstractChangeProvidingMaster<D> _underlying; /** The cache manager. */ private final CacheManager _cacheManager; /** Listens for changes in the underlying security source. */ private final ChangeListener _changeListener; /** The local change manager. */ private final ChangeManager _changeManager; /** The document cache indexed by UniqueId. */ private final Ehcache _uidToDocumentCache; /** * Creates an instance over an underlying source specifying the cache manager. * * @param name the cache name, not empty * @param underlying the underlying source, not null * @param cacheManager the cache manager, not null */ public AbstractEHCachingMaster(final String name, final AbstractChangeProvidingMaster<D> underlying, final CacheManager cacheManager) { ArgumentChecker.notEmpty(name, "name"); ArgumentChecker.notNull(underlying, "underlying"); ArgumentChecker.notNull(cacheManager, "cacheManager"); _underlying = underlying; _cacheManager = cacheManager; // Load cache configuration if (cacheManager.getCache(name + CACHE_NAME_SUFFIX) == null) { // If cache config not found, set up programmatically s_logger.warn("Could not load a cache configuration for " + name + CACHE_NAME_SUFFIX + ", building a default configuration programmatically instead"); getCacheManager().addCache(new Cache(tweakCacheConfiguration(new CacheConfiguration(name + CACHE_NAME_SUFFIX, 10000)))); } _uidToDocumentCache = new SelfPopulatingCache(_cacheManager.getCache(name + CACHE_NAME_SUFFIX), new UidToDocumentCacheEntryFactory<>(_underlying)); getCacheManager().replaceCacheWithDecoratedCache(_cacheManager.getCache(name + CACHE_NAME_SUFFIX), getUidToDocumentCache()); // Listen to change events from underlying, clean this cache accordingly and relay events to our change listeners _changeManager = new BasicChangeManager(); _changeListener = new ChangeListener() { @Override public void entityChanged(ChangeEvent event) { final ObjectId oid = event.getObjectId(); final Instant versionFrom = event.getVersionFrom(); final Instant versionTo = event.getVersionTo(); cleanCaches(oid, versionFrom, versionTo); _changeManager.entityChanged(event.getType(), event.getObjectId(), event.getVersionFrom(), event.getVersionTo(), event.getVersionInstant()); } }; underlying.changeManager().addChangeListener(_changeListener); } private CacheConfiguration tweakCacheConfiguration(CacheConfiguration cacheConfiguration) { // Set searchable index Searchable uidToDocumentCacheSearchable = new Searchable(); uidToDocumentCacheSearchable.addSearchAttribute(new SearchAttribute().name("ObjectId") .expression("value.getObjectId().toString()")); uidToDocumentCacheSearchable.addSearchAttribute(new SearchAttribute().name("VersionFromInstant") .className("com.opengamma.master.cache.InstantExtractor")); uidToDocumentCacheSearchable.addSearchAttribute(new SearchAttribute().name("VersionToInstant") .className("com.opengamma.master.cache.InstantExtractor")); uidToDocumentCacheSearchable.addSearchAttribute(new SearchAttribute().name("CorrectionFromInstant") .className("com.opengamma.master.cache.InstantExtractor")); uidToDocumentCacheSearchable.addSearchAttribute(new SearchAttribute().name("CorrectionToInstant") .className("com.opengamma.master.cache.InstantExtractor")); cacheConfiguration.addSearchable(uidToDocumentCacheSearchable); // Make copies of cached objects CopyStrategyConfiguration copyStrategyConfiguration = new CopyStrategyConfiguration(); copyStrategyConfiguration.setClass("com.opengamma.master.cache.JodaBeanCopyStrategy"); cacheConfiguration.addCopyStrategy(copyStrategyConfiguration); cacheConfiguration.setCopyOnRead(true); cacheConfiguration.setCopyOnWrite(true); cacheConfiguration.setStatistics(true); return cacheConfiguration; } //------------------------------------------------------------------------- @Override public D get(ObjectIdentifiable objectId, VersionCorrection versionCorrection) { ArgumentChecker.notNull(objectId, "objectId"); ArgumentChecker.notNull(versionCorrection, "versionCorrection"); // Search through attributes for specified oid, versions/corrections Results results = getUidToDocumentCache().createQuery() .includeKeys().includeValues() .includeAttribute(getUidToDocumentCache().getSearchAttribute("ObjectId")) .includeAttribute(getUidToDocumentCache().getSearchAttribute("VersionFromInstant")) .includeAttribute(getUidToDocumentCache().getSearchAttribute("VersionToInstant")) .includeAttribute(getUidToDocumentCache().getSearchAttribute("CorrectionFromInstant")) .includeAttribute(getUidToDocumentCache().getSearchAttribute("CorrectionToInstant")) .addCriteria(getUidToDocumentCache().getSearchAttribute("ObjectId").eq(objectId.toString())) .addCriteria(getUidToDocumentCache().getSearchAttribute("VersionFromInstant") .le(versionCorrection.withLatestFixed(InstantExtractor.MAX_INSTANT).getVersionAsOf().toString())) .addCriteria(getUidToDocumentCache().getSearchAttribute("VersionToInstant") .gt(versionCorrection.withLatestFixed(InstantExtractor.MAX_INSTANT.minusNanos(1)).getVersionAsOf().toString())) .addCriteria(getUidToDocumentCache().getSearchAttribute("CorrectionFromInstant") .le(versionCorrection.withLatestFixed(InstantExtractor.MAX_INSTANT).getCorrectedTo().toString())) .addCriteria(getUidToDocumentCache().getSearchAttribute("CorrectionToInstant") .gt(versionCorrection.withLatestFixed(InstantExtractor.MAX_INSTANT.minusNanos(1)).getCorrectedTo().toString())) .execute(); // Found a matching cached document if (results.size() == 1 && results.all().get(0).getValue() != null) { @SuppressWarnings("unchecked") D result = (D) results.all().get(0).getValue(); // Debug: check result against underlying if (TEST_AGAINST_UNDERLYING) { D check = getUnderlying().get(objectId, versionCorrection); if (!result.equals(check)) { s_logger.error(getUidToDocumentCache().getName() + " returned:\n" + result + "\nbut the underlying master returned:\n" + check); } } // Return cached value return result; // No cached document found, fetch from underlying by oid/vc instead // Note: no self-populating by oid/vc, and no caching of misses by oid/vc } else if (results.size() == 0) { // Get from underlying by oid/vc, throwing exception if not there D result = _underlying.get(objectId, versionCorrection); // Explicitly insert in cache getUidToDocumentCache().put(new Element(result.getUniqueId(), result)); return result; // Invalid result } else { throw new DataNotFoundException("Unable to uniquely identify a document with ObjectId " + objectId + " and VersionCorrection " + versionCorrection + " because more than one cached search result matches: " + results); } } @SuppressWarnings("unchecked") @Override public D get(UniqueId uniqueId) { ArgumentChecker.notNull(uniqueId, "uniqueId"); // Get from cache, which in turn self-populates from the underlying master Element element; try { element = getUidToDocumentCache().get(uniqueId); } catch (CacheException e) { throw new DataNotFoundException(e.getMessage()); } if (element != null && element.getObjectValue() != null) { // Debug: check result against underlying if (TEST_AGAINST_UNDERLYING) { D check = getUnderlying().get(uniqueId); if (!((D) element.getObjectValue()).equals(check)) { s_logger.error(getUidToDocumentCache().getName() + " returned:\n" + ((D) element.getObjectValue()) + "\nbut the underlying master returned:\n" + check); } } return (D) element.getObjectValue(); } else { throw new DataNotFoundException("No document found with the specified UniqueId"); } } @Override public Map<UniqueId, D> get(Collection<UniqueId> uniqueIds) { ArgumentChecker.notNull(uniqueIds, "uniqueIds"); Map<UniqueId, D> result = new HashMap<>(); for (UniqueId uniqueId : uniqueIds) { try { D object = get(uniqueId); result.put(uniqueId, object); } catch (DataNotFoundException ex) { // do nothing } } return result; } //------------------------------------------------------------------------- @Override public D add(D document) { ArgumentChecker.notNull(document, "document"); // Add document to underlying master D result = getUnderlying().add(document); // Store document in UniqueId cache getUidToDocumentCache().put(new Element(result.getUniqueId(), result)); return result; } @Override public D update(D document) { ArgumentChecker.notNull(document, "document"); ArgumentChecker.notNull(document.getObjectId(), "document.objectId"); // Flush previous latest version (and all its corrections) from cache cleanCaches(document.getObjectId(), Instant.now(), InstantExtractor.MAX_INSTANT); // Update document in underlying master D result = getUnderlying().update(document); // Store document in UniqueId cache getUidToDocumentCache().put(new Element(result.getUniqueId(), result)); return result; } @Override public void remove(ObjectIdentifiable objectId) { ArgumentChecker.notNull(objectId, "objectId"); // Remove document from underlying master getUnderlying().remove(objectId); // Adjust version/correction validity of latest version in Oid cache // Note: cleanCaches is already triggered by underlying master, so this is probably redundant cleanCaches(objectId.getObjectId(), Instant.now(), null); } @Override public D correct(D document) { ArgumentChecker.notNull(document, "document"); ArgumentChecker.notNull(document.getUniqueId(), "document.uniqueId"); // Flush the previous latest correction from cache getUidToDocumentCache().remove(document.getUniqueId()); // Correct document in underlying master D result = getUnderlying().correct(document); // Store latest correction in UniqueId cache getUidToDocumentCache().put(new Element(result.getUniqueId(), result)); return result; } @Override public List<UniqueId> replaceVersion(UniqueId uniqueId, List<D> replacementDocuments) { ArgumentChecker.notNull(uniqueId, "uniqueId"); ArgumentChecker.notNull(replacementDocuments, "replacementDocuments"); // Flush the original version from cache getUidToDocumentCache().remove(uniqueId); // Replace version in underlying master List<UniqueId> results = getUnderlying().replaceVersion(uniqueId, replacementDocuments); // Don't cache replacementDocuments, whose version, correction instants may have been altered by underlying master return results; } @Override public List<UniqueId> replaceAllVersions(ObjectIdentifiable objectId, List<D> replacementDocuments) { ArgumentChecker.notNull(objectId, "objectId"); ArgumentChecker.notNull(replacementDocuments, "replacementDocuments"); // Flush all existing versions from cache cleanCaches(objectId.getObjectId(), null, null); // Replace all versions in underlying master List<UniqueId> results = getUnderlying().replaceAllVersions(objectId, replacementDocuments); // Don't cache replacementDocuments, whose version, correction instants may have been altered by underlying master return results; } @Override public List<UniqueId> replaceVersions(ObjectIdentifiable objectId, List<D> replacementDocuments) { ArgumentChecker.notNull(objectId, "objectId"); ArgumentChecker.notNull(replacementDocuments, "replacementDocuments"); // Flush all existing versions from cache cleanCaches(objectId.getObjectId(), null, null); // Replace versions in underlying master List<UniqueId> results = getUnderlying().replaceVersions(objectId, replacementDocuments); // Don't cache replacementDocuments, whose version, correction instants may have been altered by underlying master return results; } @Override public UniqueId replaceVersion(D replacementDocument) { ArgumentChecker.notNull(replacementDocument, "replacementDocument"); final List<UniqueId> result = replaceVersion(replacementDocument.getUniqueId(), Collections.singletonList(replacementDocument)); if (result.isEmpty()) { return null; } else { return result.get(0); } } @Override public void removeVersion(UniqueId uniqueId) { replaceVersion(uniqueId, Collections.<D>emptyList()); } @Override public UniqueId addVersion(ObjectIdentifiable objectId, D documentToAdd) { final List<UniqueId> result = replaceVersions(objectId, Collections.singletonList(documentToAdd)); if (result.isEmpty()) { return null; } else { return result.get(0); } } //------------------------------------------------------------------------- private void cleanCaches(ObjectId objectId, Instant fromVersion, Instant toVersion) { Results results = getUidToDocumentCache().createQuery().includeKeys() .includeAttribute(getUidToDocumentCache().getSearchAttribute("ObjectId")) .includeAttribute(getUidToDocumentCache().getSearchAttribute("VersionFromInstant")) .includeAttribute(getUidToDocumentCache().getSearchAttribute("VersionToInstant")) .addCriteria(getUidToDocumentCache().getSearchAttribute("ObjectId") .eq(objectId.toString())) .addCriteria(getUidToDocumentCache().getSearchAttribute("VersionFromInstant") .le((fromVersion != null ? fromVersion : InstantExtractor.MIN_INSTANT).toString())) .addCriteria(getUidToDocumentCache().getSearchAttribute("VersionToInstant") .ge((toVersion != null ? toVersion : InstantExtractor.MAX_INSTANT).toString())) .execute(); for (Result result : results.all()) { getUidToDocumentCache().remove(result.getKey()); } } /** * Call this at the end of a unit test run to clear the state of EHCache. * It should not be part of a generic lifecycle method. */ public void shutdown() { getUnderlying().changeManager().removeChangeListener(_changeListener); getCacheManager().removeCache(getUidToDocumentCache().getName()); } //------------------------------------------------------------------------- /** * Gets the underlying source of items. * * @return the underlying source of items, not null */ protected AbstractChangeProvidingMaster<D> getUnderlying() { return _underlying; } /** * Gets the cache manager. * * @return the cache manager, not null */ protected CacheManager getCacheManager() { return _cacheManager; } /** * Gets the document by UniqueId cache. * * @return the cache, not null */ protected Ehcache getUidToDocumentCache() { return _uidToDocumentCache; } @Override public ChangeManager changeManager() { return _changeManager; } //------------------------------------------------------------------------- @Override public String toString() { return getClass().getSimpleName() + "[" + getUnderlying() + "]"; } }