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