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