/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS */ package org.opends.server.extensions; import static org.opends.messages.ExtensionMessages.*; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.util.Utils; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.server.EntryCacheCfg; import org.opends.server.admin.std.server.SoftReferenceEntryCacheCfg; import org.opends.server.api.Backend; import org.opends.server.api.DirectoryThread; import org.opends.server.api.EntryCache; import org.opends.server.core.DirectoryServer; import org.opends.server.types.Attribute; import org.opends.server.types.CacheEntry; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.SearchFilter; import org.opends.server.util.ServerConstants; /** * This class defines a Directory Server entry cache that uses soft references * to manage objects in a way that will allow them to be freed if the JVM is * running low on memory. */ public class SoftReferenceEntryCache extends EntryCache <SoftReferenceEntryCacheCfg> implements ConfigurationChangeListener<SoftReferenceEntryCacheCfg>, Runnable { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The mapping between entry DNs and their corresponding entries. */ private ConcurrentMap<DN, Reference<CacheEntry>> dnMap; /** The mapping between backend+ID and their corresponding entries. */ private ConcurrentMap<String, ConcurrentMap<Long, Reference<CacheEntry>>> idMap; /** * The reference queue that will be used to notify us whenever a soft * reference is freed. */ private ReferenceQueue<CacheEntry> referenceQueue; /** Currently registered configuration object. */ private SoftReferenceEntryCacheCfg registeredConfiguration; private Thread cleanerThread; private volatile boolean shutdown; /** * Creates a new instance of this soft reference entry cache. All * initialization should be performed in the <CODE>initializeEntryCache</CODE> * method. */ public SoftReferenceEntryCache() { super(); dnMap = new ConcurrentHashMap<>(); idMap = new ConcurrentHashMap<>(); setExcludeFilters(new HashSet<SearchFilter>()); setIncludeFilters(new HashSet<SearchFilter>()); referenceQueue = new ReferenceQueue<>(); } /** {@inheritDoc} */ @Override public void initializeEntryCache( SoftReferenceEntryCacheCfg configuration ) throws ConfigException, InitializationException { cleanerThread = new DirectoryThread(this, "Soft Reference Entry Cache Cleaner"); cleanerThread.setDaemon(true); cleanerThread.start(); registeredConfiguration = configuration; configuration.addSoftReferenceChangeListener (this); dnMap.clear(); idMap.clear(); // Read configuration and apply changes. boolean applyChanges = true; List<LocalizableMessage> errorMessages = new ArrayList<>(); EntryCacheCommon.ConfigErrorHandler errorHandler = EntryCacheCommon.getConfigErrorHandler ( EntryCacheCommon.ConfigPhase.PHASE_INIT, null, errorMessages ); if (!processEntryCacheConfig(configuration, applyChanges, errorHandler)) { String buffer = Utils.joinAsString(". ", errorMessages); throw new ConfigException(ERR_SOFTREFCACHE_CANNOT_INITIALIZE.get(buffer)); } } /** {@inheritDoc} */ @Override public synchronized void finalizeEntryCache() { registeredConfiguration.removeSoftReferenceChangeListener (this); shutdown = true; dnMap.clear(); idMap.clear(); if (cleanerThread != null) { for (int i = 0; cleanerThread.isAlive() && i < 5; i++) { cleanerThread.interrupt(); try { cleanerThread.join(10); } catch (InterruptedException e) { // We'll exit eventually. } } cleanerThread = null; } } /** {@inheritDoc} */ @Override public boolean containsEntry(DN entryDN) { return entryDN != null && dnMap.containsKey(entryDN); } /** {@inheritDoc} */ @Override public Entry getEntry(DN entryDN) { Reference<CacheEntry> ref = dnMap.get(entryDN); if (ref == null) { // Indicate cache miss. cacheMisses.getAndIncrement(); return null; } CacheEntry cacheEntry = ref.get(); if (cacheEntry == null) { // Indicate cache miss. cacheMisses.getAndIncrement(); return null; } // Indicate cache hit. cacheHits.getAndIncrement(); return cacheEntry.getEntry(); } /** {@inheritDoc} */ @Override public long getEntryID(DN entryDN) { Reference<CacheEntry> ref = dnMap.get(entryDN); if (ref != null) { CacheEntry cacheEntry = ref.get(); return cacheEntry != null ? cacheEntry.getEntryID() : -1; } return -1; } /** {@inheritDoc} */ @Override public DN getEntryDN(String backendID, long entryID) { // Locate specific backend map and return the entry DN by ID. ConcurrentMap<Long, Reference<CacheEntry>> backendMap = idMap.get(backendID); if (backendMap != null) { Reference<CacheEntry> ref = backendMap.get(entryID); if (ref != null) { CacheEntry cacheEntry = ref.get(); if (cacheEntry != null) { return cacheEntry.getDN(); } } } return null; } /** {@inheritDoc} */ @Override public void putEntry(Entry entry, String backendID, long entryID) { // Create the cache entry based on the provided information. CacheEntry cacheEntry = new CacheEntry(entry, backendID, entryID); Reference<CacheEntry> ref = new SoftReference<>(cacheEntry, referenceQueue); Reference<CacheEntry> oldRef = dnMap.put(entry.getName(), ref); if (oldRef != null) { oldRef.clear(); } ConcurrentMap<Long,Reference<CacheEntry>> map = idMap.get(backendID); if (map == null) { map = new ConcurrentHashMap<>(); map.put(entryID, ref); idMap.put(backendID, map); } else { oldRef = map.put(entryID, ref); if (oldRef != null) { oldRef.clear(); } } } /** {@inheritDoc} */ @Override public boolean putEntryIfAbsent(Entry entry, String backendID, long entryID) { // See if the entry already exists. If so, then return false. if (dnMap.containsKey(entry.getName())) { return false; } // Create the cache entry based on the provided information. CacheEntry cacheEntry = new CacheEntry(entry, backendID, entryID); Reference<CacheEntry> ref = new SoftReference<>(cacheEntry, referenceQueue); dnMap.put(entry.getName(), ref); ConcurrentMap<Long,Reference<CacheEntry>> map = idMap.get(backendID); if (map == null) { map = new ConcurrentHashMap<>(); map.put(entryID, ref); idMap.put(backendID, map); } else { map.put(entryID, ref); } return true; } /** {@inheritDoc} */ @Override public void removeEntry(DN entryDN) { Reference<CacheEntry> ref = dnMap.remove(entryDN); if (ref != null) { ref.clear(); CacheEntry cacheEntry = ref.get(); if (cacheEntry != null) { final String backendID = cacheEntry.getBackendID(); ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.get(backendID); if (map != null) { ref = map.remove(cacheEntry.getEntryID()); if (ref != null) { ref.clear(); } // If this backend becomes empty now remove // it from the idMap map. if (map.isEmpty()) { idMap.remove(backendID); } } } } } /** {@inheritDoc} */ @Override public void clear() { dnMap.clear(); idMap.clear(); } /** {@inheritDoc} */ @Override public void clearBackend(String backendID) { // FIXME -- Would it be better just to dump everything? final ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.remove(backendID); if (map != null) { for (Reference<CacheEntry> ref : map.values()) { final CacheEntry cacheEntry = ref.get(); if (cacheEntry != null) { dnMap.remove(cacheEntry.getDN()); } ref.clear(); } map.clear(); } } /** {@inheritDoc} */ @Override public void clearSubtree(DN baseDN) { // Determine the backend used to hold the specified base DN and clear it. Backend<?> backend = DirectoryServer.getBackend(baseDN); if (backend == null) { // FIXME -- Should we clear everything just to be safe? } else { clearBackend(backend.getBackendID()); } } /** {@inheritDoc} */ @Override public void handleLowMemory() { // This function should automatically be taken care of by the nature of the // soft references used in this cache. // FIXME -- Do we need to do anything at all here? } /** {@inheritDoc} */ @Override public boolean isConfigurationAcceptable(EntryCacheCfg configuration, List<LocalizableMessage> unacceptableReasons) { SoftReferenceEntryCacheCfg config = (SoftReferenceEntryCacheCfg) configuration; return isConfigurationChangeAcceptable(config, unacceptableReasons); } /** {@inheritDoc} */ @Override public boolean isConfigurationChangeAcceptable( SoftReferenceEntryCacheCfg configuration, List<LocalizableMessage> unacceptableReasons) { boolean applyChanges = false; EntryCacheCommon.ConfigErrorHandler errorHandler = EntryCacheCommon.getConfigErrorHandler ( EntryCacheCommon.ConfigPhase.PHASE_ACCEPTABLE, unacceptableReasons, null ); processEntryCacheConfig (configuration, applyChanges, errorHandler); return errorHandler.getIsAcceptable(); } /** {@inheritDoc} */ @Override public ConfigChangeResult applyConfigurationChange(SoftReferenceEntryCacheCfg configuration) { boolean applyChanges = true; List<LocalizableMessage> errorMessages = new ArrayList<>(); EntryCacheCommon.ConfigErrorHandler errorHandler = EntryCacheCommon.getConfigErrorHandler ( EntryCacheCommon.ConfigPhase.PHASE_APPLY, null, errorMessages ); // Do not apply changes unless this cache is enabled. if (configuration.isEnabled()) { processEntryCacheConfig (configuration, applyChanges, errorHandler); } final ConfigChangeResult changeResult = new ConfigChangeResult(); changeResult.setResultCode(errorHandler.getResultCode()); changeResult.setAdminActionRequired(errorHandler.getIsAdminActionRequired()); changeResult.getMessages().addAll(errorHandler.getErrorMessages()); return changeResult; } /** * Parses the provided configuration and configure the entry cache. * * @param configuration The new configuration containing the changes. * @param applyChanges If true then take into account the new configuration. * @param errorHandler An handler used to report errors. * * @return <CODE>true</CODE> if configuration is acceptable, * or <CODE>false</CODE> otherwise. */ public boolean processEntryCacheConfig( SoftReferenceEntryCacheCfg configuration, boolean applyChanges, EntryCacheCommon.ConfigErrorHandler errorHandler ) { // Local variables to read configuration. DN newConfigEntryDN; Set<SearchFilter> newIncludeFilters = null; Set<SearchFilter> newExcludeFilters = null; // Read configuration. newConfigEntryDN = configuration.dn(); // Get include and exclude filters. switch (errorHandler.getConfigPhase()) { case PHASE_INIT: case PHASE_ACCEPTABLE: case PHASE_APPLY: newIncludeFilters = EntryCacheCommon.getFilters ( configuration.getIncludeFilter(), ERR_CACHE_INVALID_INCLUDE_FILTER, errorHandler, newConfigEntryDN ); newExcludeFilters = EntryCacheCommon.getFilters ( configuration.getExcludeFilter(), ERR_CACHE_INVALID_EXCLUDE_FILTER, errorHandler, newConfigEntryDN ); break; } if (applyChanges && errorHandler.getIsAcceptable()) { setIncludeFilters(newIncludeFilters); setExcludeFilters(newExcludeFilters); registeredConfiguration = configuration; } return errorHandler.getIsAcceptable(); } /** * Operate in a loop, receiving notification of soft references that have been * freed and removing the corresponding entries from the cache. */ @Override public void run() { while (!shutdown) { try { CacheEntry freedEntry = referenceQueue.remove().get(); if (freedEntry != null) { Reference<CacheEntry> ref = dnMap.remove(freedEntry.getDN()); if (ref != null) { // Note that the entry is there, but it could be a newer version of // the entry so we want to make sure it's the same one. CacheEntry removedEntry = ref.get(); if (removedEntry != freedEntry) { dnMap.putIfAbsent(freedEntry.getDN(), ref); } else { ref.clear(); final String backendID = freedEntry.getBackendID(); final ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.get(backendID); if (map != null) { ref = map.remove(freedEntry.getEntryID()); if (ref != null) { ref.clear(); } // If this backend becomes empty now remove // it from the idMap map. if (map.isEmpty()) { idMap.remove(backendID); } } } } } } catch (Exception e) { logger.traceException(e); } } } /** {@inheritDoc} */ @Override public List<Attribute> getMonitorData() { try { return EntryCacheCommon.getGenericMonitorData( Long.valueOf(cacheHits.longValue()), // If cache misses is maintained by default cache // get it from there and if not point to itself. DirectoryServer.getEntryCache().getCacheMisses(), null, null, Long.valueOf(dnMap.size()), null ); } catch (Exception e) { logger.traceException(e); return Collections.emptyList(); } } /** {@inheritDoc} */ @Override public Long getCacheCount() { return Long.valueOf(dnMap.size()); } /** {@inheritDoc} */ @Override public String toVerboseString() { StringBuilder sb = new StringBuilder(); // There're no locks in this cache to keep dnMap and idMap in sync. // Examine dnMap only since its more likely to be up to date than idMap. // Do not bother with copies either since this // is SoftReference based implementation. for(Reference<CacheEntry> ce : dnMap.values()) { sb.append(ce.get().getDN()); sb.append(":"); sb.append(ce.get().getEntryID()); sb.append(":"); sb.append(ce.get().getBackendID()); sb.append(ServerConstants.EOL); } String verboseString = sb.toString(); return verboseString.length() > 0 ? verboseString : null; } }