001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.extensions;
028
029import static org.opends.messages.ExtensionMessages.*;
030
031import java.lang.ref.Reference;
032import java.lang.ref.ReferenceQueue;
033import java.lang.ref.SoftReference;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Set;
039import java.util.concurrent.ConcurrentHashMap;
040import java.util.concurrent.ConcurrentMap;
041
042import org.forgerock.i18n.LocalizableMessage;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044import org.forgerock.opendj.config.server.ConfigException;
045import org.forgerock.util.Utils;
046import org.opends.server.admin.server.ConfigurationChangeListener;
047import org.opends.server.admin.std.server.EntryCacheCfg;
048import org.opends.server.admin.std.server.SoftReferenceEntryCacheCfg;
049import org.opends.server.api.Backend;
050import org.opends.server.api.DirectoryThread;
051import org.opends.server.api.EntryCache;
052import org.opends.server.core.DirectoryServer;
053import org.opends.server.types.Attribute;
054import org.opends.server.types.CacheEntry;
055import org.forgerock.opendj.config.server.ConfigChangeResult;
056import org.opends.server.types.DN;
057import org.opends.server.types.Entry;
058import org.opends.server.types.InitializationException;
059import org.opends.server.types.SearchFilter;
060import org.opends.server.util.ServerConstants;
061
062/**
063 * This class defines a Directory Server entry cache that uses soft references
064 * to manage objects in a way that will allow them to be freed if the JVM is
065 * running low on memory.
066 */
067public class SoftReferenceEntryCache
068    extends EntryCache <SoftReferenceEntryCacheCfg>
069    implements
070        ConfigurationChangeListener<SoftReferenceEntryCacheCfg>,
071        Runnable
072{
073  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
074
075  /** The mapping between entry DNs and their corresponding entries. */
076  private ConcurrentMap<DN, Reference<CacheEntry>> dnMap;
077
078  /** The mapping between backend+ID and their corresponding entries. */
079  private ConcurrentMap<String, ConcurrentMap<Long, Reference<CacheEntry>>> idMap;
080
081  /**
082   * The reference queue that will be used to notify us whenever a soft
083   * reference is freed.
084   */
085  private ReferenceQueue<CacheEntry> referenceQueue;
086
087  /** Currently registered configuration object. */
088  private SoftReferenceEntryCacheCfg registeredConfiguration;
089
090  private Thread cleanerThread;
091  private volatile boolean shutdown;
092
093
094
095  /**
096   * Creates a new instance of this soft reference entry cache.  All
097   * initialization should be performed in the <CODE>initializeEntryCache</CODE>
098   * method.
099   */
100  public SoftReferenceEntryCache()
101  {
102    super();
103
104    dnMap = new ConcurrentHashMap<>();
105    idMap = new ConcurrentHashMap<>();
106
107    setExcludeFilters(new HashSet<SearchFilter>());
108    setIncludeFilters(new HashSet<SearchFilter>());
109    referenceQueue = new ReferenceQueue<>();
110  }
111
112  /** {@inheritDoc} */
113  @Override
114  public void initializeEntryCache(
115      SoftReferenceEntryCacheCfg configuration
116      )
117      throws ConfigException, InitializationException
118  {
119    cleanerThread = new DirectoryThread(this,
120        "Soft Reference Entry Cache Cleaner");
121    cleanerThread.setDaemon(true);
122    cleanerThread.start();
123
124    registeredConfiguration = configuration;
125    configuration.addSoftReferenceChangeListener (this);
126
127    dnMap.clear();
128    idMap.clear();
129
130    // Read configuration and apply changes.
131    boolean applyChanges = true;
132    List<LocalizableMessage> errorMessages = new ArrayList<>();
133    EntryCacheCommon.ConfigErrorHandler errorHandler =
134      EntryCacheCommon.getConfigErrorHandler (
135          EntryCacheCommon.ConfigPhase.PHASE_INIT, null, errorMessages
136          );
137    if (!processEntryCacheConfig(configuration, applyChanges, errorHandler)) {
138      String buffer = Utils.joinAsString(".  ", errorMessages);
139      throw new ConfigException(ERR_SOFTREFCACHE_CANNOT_INITIALIZE.get(buffer));
140    }
141  }
142
143  /** {@inheritDoc} */
144  @Override
145  public synchronized void finalizeEntryCache()
146  {
147    registeredConfiguration.removeSoftReferenceChangeListener (this);
148
149    shutdown = true;
150
151    dnMap.clear();
152    idMap.clear();
153    if (cleanerThread != null) {
154      for (int i = 0; cleanerThread.isAlive() && i < 5; i++) {
155        cleanerThread.interrupt();
156        try {
157          cleanerThread.join(10);
158        } catch (InterruptedException e) {
159          // We'll exit eventually.
160        }
161      }
162      cleanerThread = null;
163    }
164  }
165
166  /** {@inheritDoc} */
167  @Override
168  public boolean containsEntry(DN entryDN)
169  {
170    return entryDN != null && dnMap.containsKey(entryDN);
171  }
172
173  /** {@inheritDoc} */
174  @Override
175  public Entry getEntry(DN entryDN)
176  {
177    Reference<CacheEntry> ref = dnMap.get(entryDN);
178    if (ref == null)
179    {
180      // Indicate cache miss.
181      cacheMisses.getAndIncrement();
182      return null;
183    }
184    CacheEntry cacheEntry = ref.get();
185    if (cacheEntry == null)
186    {
187      // Indicate cache miss.
188      cacheMisses.getAndIncrement();
189      return null;
190    }
191    // Indicate cache hit.
192    cacheHits.getAndIncrement();
193    return cacheEntry.getEntry();
194  }
195
196  /** {@inheritDoc} */
197  @Override
198  public long getEntryID(DN entryDN)
199  {
200    Reference<CacheEntry> ref = dnMap.get(entryDN);
201    if (ref != null)
202    {
203      CacheEntry cacheEntry = ref.get();
204      return cacheEntry != null ? cacheEntry.getEntryID() : -1;
205    }
206    return -1;
207  }
208
209  /** {@inheritDoc} */
210  @Override
211  public DN getEntryDN(String backendID, long entryID)
212  {
213    // Locate specific backend map and return the entry DN by ID.
214    ConcurrentMap<Long, Reference<CacheEntry>> backendMap = idMap.get(backendID);
215    if (backendMap != null) {
216      Reference<CacheEntry> ref = backendMap.get(entryID);
217      if (ref != null) {
218        CacheEntry cacheEntry = ref.get();
219        if (cacheEntry != null) {
220          return cacheEntry.getDN();
221        }
222      }
223    }
224    return null;
225  }
226
227  /** {@inheritDoc} */
228  @Override
229  public void putEntry(Entry entry, String backendID, long entryID)
230  {
231    // Create the cache entry based on the provided information.
232    CacheEntry cacheEntry = new CacheEntry(entry, backendID, entryID);
233    Reference<CacheEntry> ref = new SoftReference<>(cacheEntry, referenceQueue);
234
235    Reference<CacheEntry> oldRef = dnMap.put(entry.getName(), ref);
236    if (oldRef != null)
237    {
238      oldRef.clear();
239    }
240
241    ConcurrentMap<Long,Reference<CacheEntry>> map = idMap.get(backendID);
242    if (map == null)
243    {
244      map = new ConcurrentHashMap<>();
245      map.put(entryID, ref);
246      idMap.put(backendID, map);
247    }
248    else
249    {
250      oldRef = map.put(entryID, ref);
251      if (oldRef != null)
252      {
253        oldRef.clear();
254      }
255    }
256  }
257
258  /** {@inheritDoc} */
259  @Override
260  public boolean putEntryIfAbsent(Entry entry, String backendID, long entryID)
261  {
262    // See if the entry already exists.  If so, then return false.
263    if (dnMap.containsKey(entry.getName()))
264    {
265      return false;
266    }
267
268
269    // Create the cache entry based on the provided information.
270    CacheEntry cacheEntry = new CacheEntry(entry, backendID, entryID);
271    Reference<CacheEntry> ref = new SoftReference<>(cacheEntry, referenceQueue);
272
273    dnMap.put(entry.getName(), ref);
274
275    ConcurrentMap<Long,Reference<CacheEntry>> map = idMap.get(backendID);
276    if (map == null)
277    {
278      map = new ConcurrentHashMap<>();
279      map.put(entryID, ref);
280      idMap.put(backendID, map);
281    }
282    else
283    {
284      map.put(entryID, ref);
285    }
286
287    return true;
288  }
289
290  /** {@inheritDoc} */
291  @Override
292  public void removeEntry(DN entryDN)
293  {
294    Reference<CacheEntry> ref = dnMap.remove(entryDN);
295    if (ref != null)
296    {
297      ref.clear();
298
299      CacheEntry cacheEntry = ref.get();
300      if (cacheEntry != null)
301      {
302        final String backendID = cacheEntry.getBackendID();
303
304        ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.get(backendID);
305        if (map != null)
306        {
307          ref = map.remove(cacheEntry.getEntryID());
308          if (ref != null)
309          {
310            ref.clear();
311          }
312          // If this backend becomes empty now remove
313          // it from the idMap map.
314          if (map.isEmpty())
315          {
316            idMap.remove(backendID);
317          }
318        }
319      }
320    }
321  }
322
323  /** {@inheritDoc} */
324  @Override
325  public void clear()
326  {
327    dnMap.clear();
328    idMap.clear();
329  }
330
331  /** {@inheritDoc} */
332  @Override
333  public void clearBackend(String backendID)
334  {
335    // FIXME -- Would it be better just to dump everything?
336    final ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.remove(backendID);
337    if (map != null)
338    {
339      for (Reference<CacheEntry> ref : map.values())
340      {
341        final CacheEntry cacheEntry = ref.get();
342        if (cacheEntry != null)
343        {
344          dnMap.remove(cacheEntry.getDN());
345        }
346
347        ref.clear();
348      }
349
350      map.clear();
351    }
352  }
353
354  /** {@inheritDoc} */
355  @Override
356  public void clearSubtree(DN baseDN)
357  {
358    // Determine the backend used to hold the specified base DN and clear it.
359    Backend<?> backend = DirectoryServer.getBackend(baseDN);
360    if (backend == null)
361    {
362      // FIXME -- Should we clear everything just to be safe?
363    }
364    else
365    {
366      clearBackend(backend.getBackendID());
367    }
368  }
369
370  /** {@inheritDoc} */
371  @Override
372  public void handleLowMemory()
373  {
374    // This function should automatically be taken care of by the nature of the
375    // soft references used in this cache.
376    // FIXME -- Do we need to do anything at all here?
377  }
378
379  /** {@inheritDoc} */
380  @Override
381  public boolean isConfigurationAcceptable(EntryCacheCfg configuration,
382                                           List<LocalizableMessage> unacceptableReasons)
383  {
384    SoftReferenceEntryCacheCfg config =
385         (SoftReferenceEntryCacheCfg) configuration;
386    return isConfigurationChangeAcceptable(config, unacceptableReasons);
387  }
388
389  /** {@inheritDoc} */
390  @Override
391  public boolean isConfigurationChangeAcceptable(
392      SoftReferenceEntryCacheCfg configuration,
393      List<LocalizableMessage> unacceptableReasons)
394  {
395    boolean applyChanges = false;
396    EntryCacheCommon.ConfigErrorHandler errorHandler =
397      EntryCacheCommon.getConfigErrorHandler (
398          EntryCacheCommon.ConfigPhase.PHASE_ACCEPTABLE,
399          unacceptableReasons,
400          null
401        );
402    processEntryCacheConfig (configuration, applyChanges, errorHandler);
403
404    return errorHandler.getIsAcceptable();
405  }
406
407  /** {@inheritDoc} */
408  @Override
409  public ConfigChangeResult applyConfigurationChange(SoftReferenceEntryCacheCfg configuration)
410  {
411    boolean applyChanges = true;
412    List<LocalizableMessage> errorMessages = new ArrayList<>();
413    EntryCacheCommon.ConfigErrorHandler errorHandler =
414      EntryCacheCommon.getConfigErrorHandler (
415          EntryCacheCommon.ConfigPhase.PHASE_APPLY, null, errorMessages
416          );
417    // Do not apply changes unless this cache is enabled.
418    if (configuration.isEnabled()) {
419      processEntryCacheConfig (configuration, applyChanges, errorHandler);
420    }
421
422    final ConfigChangeResult changeResult = new ConfigChangeResult();
423    changeResult.setResultCode(errorHandler.getResultCode());
424    changeResult.setAdminActionRequired(errorHandler.getIsAdminActionRequired());
425    changeResult.getMessages().addAll(errorHandler.getErrorMessages());
426    return changeResult;
427  }
428
429
430
431  /**
432   * Parses the provided configuration and configure the entry cache.
433   *
434   * @param configuration  The new configuration containing the changes.
435   * @param applyChanges   If true then take into account the new configuration.
436   * @param errorHandler   An handler used to report errors.
437   *
438   * @return  <CODE>true</CODE> if configuration is acceptable,
439   *          or <CODE>false</CODE> otherwise.
440   */
441  public boolean processEntryCacheConfig(
442      SoftReferenceEntryCacheCfg          configuration,
443      boolean                             applyChanges,
444      EntryCacheCommon.ConfigErrorHandler errorHandler
445      )
446  {
447    // Local variables to read configuration.
448    DN newConfigEntryDN;
449    Set<SearchFilter> newIncludeFilters = null;
450    Set<SearchFilter> newExcludeFilters = null;
451
452    // Read configuration.
453    newConfigEntryDN = configuration.dn();
454
455    // Get include and exclude filters.
456    switch (errorHandler.getConfigPhase())
457    {
458    case PHASE_INIT:
459    case PHASE_ACCEPTABLE:
460    case PHASE_APPLY:
461      newIncludeFilters = EntryCacheCommon.getFilters (
462          configuration.getIncludeFilter(),
463          ERR_CACHE_INVALID_INCLUDE_FILTER,
464          errorHandler,
465          newConfigEntryDN
466          );
467      newExcludeFilters = EntryCacheCommon.getFilters (
468          configuration.getExcludeFilter(),
469          ERR_CACHE_INVALID_EXCLUDE_FILTER,
470          errorHandler,
471          newConfigEntryDN
472          );
473      break;
474    }
475
476    if (applyChanges && errorHandler.getIsAcceptable())
477    {
478      setIncludeFilters(newIncludeFilters);
479      setExcludeFilters(newExcludeFilters);
480
481      registeredConfiguration = configuration;
482    }
483
484    return errorHandler.getIsAcceptable();
485  }
486
487  /**
488   * Operate in a loop, receiving notification of soft references that have been
489   * freed and removing the corresponding entries from the cache.
490   */
491  @Override
492  public void run()
493  {
494    while (!shutdown)
495    {
496      try
497      {
498        CacheEntry freedEntry = referenceQueue.remove().get();
499
500        if (freedEntry != null)
501        {
502          Reference<CacheEntry> ref = dnMap.remove(freedEntry.getDN());
503
504          if (ref != null)
505          {
506            // Note that the entry is there, but it could be a newer version of
507            // the entry so we want to make sure it's the same one.
508            CacheEntry removedEntry = ref.get();
509            if (removedEntry != freedEntry)
510            {
511              dnMap.putIfAbsent(freedEntry.getDN(), ref);
512            }
513            else
514            {
515              ref.clear();
516
517              final String backendID = freedEntry.getBackendID();
518              final ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.get(backendID);
519              if (map != null)
520              {
521                ref = map.remove(freedEntry.getEntryID());
522                if (ref != null)
523                {
524                  ref.clear();
525                }
526                // If this backend becomes empty now remove
527                // it from the idMap map.
528                if (map.isEmpty()) {
529                  idMap.remove(backendID);
530                }
531              }
532            }
533          }
534        }
535      }
536      catch (Exception e)
537      {
538        logger.traceException(e);
539      }
540    }
541  }
542
543  /** {@inheritDoc} */
544  @Override
545  public List<Attribute> getMonitorData()
546  {
547    try {
548      return EntryCacheCommon.getGenericMonitorData(
549        Long.valueOf(cacheHits.longValue()),
550        // If cache misses is maintained by default cache
551        // get it from there and if not point to itself.
552        DirectoryServer.getEntryCache().getCacheMisses(),
553        null,
554        null,
555        Long.valueOf(dnMap.size()),
556        null
557        );
558    } catch (Exception e) {
559      logger.traceException(e);
560      return Collections.emptyList();
561    }
562  }
563
564  /** {@inheritDoc} */
565  @Override
566  public Long getCacheCount()
567  {
568    return Long.valueOf(dnMap.size());
569  }
570
571  /** {@inheritDoc} */
572  @Override
573  public String toVerboseString()
574  {
575    StringBuilder sb = new StringBuilder();
576
577    // There're no locks in this cache to keep dnMap and idMap in sync.
578    // Examine dnMap only since its more likely to be up to date than idMap.
579    // Do not bother with copies either since this
580    // is SoftReference based implementation.
581    for(Reference<CacheEntry> ce : dnMap.values()) {
582      sb.append(ce.get().getDN());
583      sb.append(":");
584      sb.append(ce.get().getEntryID());
585      sb.append(":");
586      sb.append(ce.get().getBackendID());
587      sb.append(ServerConstants.EOL);
588    }
589
590    String verboseString = sb.toString();
591    return verboseString.length() > 0 ? verboseString : null;
592  }
593}