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