View Javadoc

1   package org.apache.jcs.auxiliary.disk.block;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.Serializable;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.concurrent.ScheduledExecutorService;
34  import java.util.concurrent.TimeUnit;
35  import java.util.concurrent.locks.ReentrantReadWriteLock;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.jcs.auxiliary.AuxiliaryCacheAttributes;
40  import org.apache.jcs.auxiliary.disk.AbstractDiskCache;
41  import org.apache.jcs.engine.CacheConstants;
42  import org.apache.jcs.engine.behavior.ICacheElement;
43  import org.apache.jcs.engine.behavior.IElementSerializer;
44  import org.apache.jcs.engine.behavior.IRequireScheduler;
45  import org.apache.jcs.engine.control.group.GroupAttrName;
46  import org.apache.jcs.engine.control.group.GroupId;
47  import org.apache.jcs.engine.stats.StatElement;
48  import org.apache.jcs.engine.stats.Stats;
49  import org.apache.jcs.engine.stats.behavior.IStatElement;
50  import org.apache.jcs.engine.stats.behavior.IStats;
51  
52  /**
53   * There is one BlockDiskCache per region. It manages the key and data store.
54   * <p>
55   * @author Aaron Smuts
56   */
57  public class BlockDiskCache<K extends Serializable, V extends Serializable>
58      extends AbstractDiskCache<K, V>
59      implements IRequireScheduler
60  {
61      /** Don't change */
62      private static final long serialVersionUID = 1L;
63  
64      /** The logger. */
65      protected static final Log log = LogFactory.getLog( BlockDiskCache.class );
66  
67      /** The name to prefix all log messages with. */
68      private final String logCacheName;
69  
70      /** The name of the file to store data. */
71      private final String fileName;
72  
73      /** The data access object */
74      private BlockDisk dataFile;
75  
76      /** Attributes governing the behavior of the block disk cache. */
77      private final BlockDiskCacheAttributes blockDiskCacheAttributes;
78  
79      /** The root directory for keys and data. */
80      private final File rootDirectory;
81  
82      /** Store, loads, and persists the keys */
83      protected BlockDiskKeyStore<K> keyStore;
84  
85      /**
86       * Use this lock to synchronize reads and writes to the underlying storage mechanism. We don't
87       * need a reentrant lock, since we only lock one level.
88       */
89      private final ReentrantReadWriteLock storageLock = new ReentrantReadWriteLock();
90  
91      /**
92       * Constructs the BlockDisk after setting up the root directory.
93       * <p>
94       * @param cacheAttributes
95       */
96      public BlockDiskCache( BlockDiskCacheAttributes cacheAttributes )
97      {
98          this( cacheAttributes, null );
99      }
100 
101     /**
102      * Constructs the BlockDisk after setting up the root directory.
103      * <p>
104      * @param cacheAttributes
105      * @param elementSerializer used if supplied, the super's super will not set a null
106      */
107     public BlockDiskCache( BlockDiskCacheAttributes cacheAttributes, IElementSerializer elementSerializer )
108     {
109         super( cacheAttributes );
110         setElementSerializer( elementSerializer );
111 
112         this.blockDiskCacheAttributes = cacheAttributes;
113         this.logCacheName = "Region [" + getCacheName() + "] ";
114 
115         if ( log.isInfoEnabled() )
116         {
117             log.info( logCacheName + "Constructing BlockDiskCache with attributes " + cacheAttributes );
118         }
119 
120         this.fileName = getCacheName();
121         String rootDirName = cacheAttributes.getDiskPath();
122         this.rootDirectory = new File( rootDirName );
123         this.rootDirectory.mkdirs();
124 
125         if ( log.isInfoEnabled() )
126         {
127             log.info( logCacheName + "Cache file root directory: [" + rootDirName + "]" );
128         }
129 
130         try
131         {
132             if ( this.blockDiskCacheAttributes.getBlockSizeBytes() > 0 )
133             {
134                 this.dataFile = new BlockDisk( new File( rootDirectory, fileName + ".data" ),
135                                                this.blockDiskCacheAttributes.getBlockSizeBytes() );
136             }
137             else
138             {
139                 this.dataFile = new BlockDisk( new File( rootDirectory, fileName + ".data" ), getElementSerializer() );
140             }
141 
142             keyStore = new BlockDiskKeyStore<K>( this.blockDiskCacheAttributes, this );
143 
144             boolean alright = verifyDisk();
145 
146             if ( keyStore.size() == 0 || !alright )
147             {
148                 this.reset();
149             }
150 
151             // Initialization finished successfully, so set alive to true.
152             alive = true;
153             if ( log.isInfoEnabled() )
154             {
155                 log.info( logCacheName + "Block Disk Cache is alive." );
156             }
157         }
158         catch ( IOException e )
159         {
160             log.error( logCacheName + "Failure initializing for fileName: " + fileName + " and root directory: "
161                 + rootDirName, e );
162         }
163     }
164 
165     /**
166      * @see org.apache.jcs.engine.behavior.IRequireScheduler#setScheduledExecutorService(java.util.concurrent.ScheduledExecutorService)
167      */
168     public void setScheduledExecutorService(ScheduledExecutorService scheduledExecutor)
169     {
170         // add this region to the persistence thread.
171         // TODO we might need to stagger this a bit.
172         if ( this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds() > 0 )
173         {
174             scheduledExecutor.scheduleAtFixedRate(
175                     new Runnable()
176                     {
177                         public void run()
178                         {
179                             keyStore.saveKeys();
180                         }
181                     },
182                     this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds(),
183                     this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds(),
184                     TimeUnit.SECONDS);
185         }
186     }
187 
188     /**
189      * We need to verify that the file on disk uses the same block size and that the file is the
190      * proper size.
191      * <p>
192      * @return true if it looks ok
193      */
194     protected boolean verifyDisk()
195     {
196         boolean alright = false;
197         // simply try to read a few. If it works, then the file is probably ok.
198         // TODO add more.
199 
200         storageLock.readLock().lock();
201 
202         try
203         {
204             int maxToTest = 100;
205             int count = 0;
206             Iterator<Map.Entry<K, int[]>> it = this.keyStore.entrySet().iterator();
207             while ( it.hasNext() && count < maxToTest )
208             {
209                 count++;
210                 Map.Entry<K, int[]> entry = it.next();
211                 Object data = this.dataFile.read( entry.getValue() );
212                 if ( data == null )
213                 {
214                     throw new Exception( logCacheName + "Couldn't find data for key [" + entry.getKey() + "]" );
215                 }
216             }
217             alright = true;
218         }
219         catch ( Exception e )
220         {
221             log.warn( logCacheName + "Problem verifying disk.  Message [" + e.getMessage() + "]" );
222             alright = false;
223         }
224         finally
225         {
226             storageLock.readLock().unlock();
227         }
228 
229         return alright;
230     }
231 
232     /**
233      * This requires a full iteration through the keys.
234      * <p>
235      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#getGroupKeys(java.lang.String)
236      */
237     @Override
238     public Set<K> getGroupKeys( String groupName )
239     {
240         GroupId groupId = new GroupId( cacheName, groupName );
241         HashSet<K> keys = new HashSet<K>();
242 
243         storageLock.readLock().lock();
244 
245         try
246         {
247             for ( K key : this.keyStore.keySet())
248             {
249                 if ( key instanceof GroupAttrName && ( (GroupAttrName<?>) key ).groupId.equals( groupId ) )
250                 {
251                     @SuppressWarnings("unchecked") // Type checked with instanceof
252                     K newKey = ((GroupAttrName<K>) key ).attrName;
253                     keys.add( newKey );
254                 }
255             }
256         }
257         finally
258         {
259             storageLock.readLock().unlock();
260         }
261 
262         return keys;
263     }
264 
265     /**
266      * This requires a full iteration through the keys.
267      * <p>
268      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#getGroupNames()
269      */
270     @Override
271     public Set<String> getGroupNames()
272     {
273         HashSet<String> names = new HashSet<String>();
274 
275         storageLock.readLock().lock();
276 
277         try
278         {
279             for ( Serializable key : this.keyStore.keySet())
280             {
281                 if ( key instanceof GroupAttrName )
282                 {
283                     @SuppressWarnings("unchecked") // Type checked with instanceof
284                     GroupId groupID = ((GroupAttrName<K>) key ).groupId;
285                     names.add( groupID.groupName );
286                 }
287             }
288         }
289         finally
290         {
291             storageLock.readLock().unlock();
292         }
293 
294         return names;
295     }
296 
297     /**
298      * Gets matching items from the cache.
299      * <p>
300      * @param pattern
301      * @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
302      *         data in cache matching keys
303      */
304     @Override
305     public Map<K, ICacheElement<K, V>> processGetMatching( String pattern )
306     {
307         Map<K, ICacheElement<K, V>> elements = new HashMap<K, ICacheElement<K, V>>();
308 
309         Set<K> keyArray = null;
310         storageLock.readLock().lock();
311         try
312         {
313             keyArray = new HashSet<K>(keyStore.keySet());
314         }
315         finally
316         {
317             storageLock.readLock().unlock();
318         }
319 
320         Set<K> matchingKeys = getKeyMatcher().getMatchingKeysFromArray( pattern, keyArray );
321 
322         for (K key : matchingKeys)
323         {
324             ICacheElement<K, V> element = processGet( key );
325             if ( element != null )
326             {
327                 elements.put( key, element );
328             }
329         }
330 
331         return elements;
332     }
333 
334     /**
335      * Returns the number of keys.
336      * <p>
337      * (non-Javadoc)
338      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#getSize()
339      */
340     @Override
341     public int getSize()
342     {
343         return this.keyStore.size();
344     }
345 
346     /**
347      * Gets the ICacheElement<K, V> for the key if it is in the cache. The program flow is as follows:
348      * <ol>
349      * <li>Make sure the disk cache is alive.</li> <li>Get a read lock.</li> <li>See if the key is
350      * in the key store.</li> <li>If we found a key, ask the BlockDisk for the object at the
351      * blocks..</li> <li>Release the lock.</li>
352      * </ol>
353      * (non-Javadoc)
354      * @param key
355      * @return ICacheElement
356      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doGet(java.io.Serializable)
357      */
358     @Override
359     protected ICacheElement<K, V> processGet( K key )
360     {
361         if ( !alive )
362         {
363             if ( log.isDebugEnabled() )
364             {
365                 log.debug( logCacheName + "No longer alive so returning null for key = " + key );
366             }
367             return null;
368         }
369 
370         if ( log.isDebugEnabled() )
371         {
372             log.debug( logCacheName + "Trying to get from disk: " + key );
373         }
374 
375         ICacheElement<K, V> object = null;
376         storageLock.readLock().lock();
377 
378         try
379         {
380             int[] ded = this.keyStore.get( key );
381             if ( ded != null )
382             {
383                 object = this.dataFile.read( ded );
384             }
385         }
386         catch ( IOException ioe )
387         {
388             log.error( logCacheName + "Failure getting from disk--IOException, key = " + key, ioe );
389             reset();
390         }
391         catch ( Exception e )
392         {
393             log.error( logCacheName + "Failure getting from disk, key = " + key, e );
394         }
395         finally
396         {
397             storageLock.readLock().unlock();
398         }
399 
400         return object;
401     }
402 
403     /**
404      * Writes an element to disk. The program flow is as follows:
405      * <ol>
406      * <li>Acquire write lock.</li> <li>See id an item exists for this key.</li> <li>If an item
407      * already exists, add its blocks to the remove list.</li> <li>Have the Block disk write the
408      * item.</li> <li>Create a descriptor and add it to the key map.</li> <li>Release the write
409      * lock.</li>
410      * </ol>
411      * @param element
412      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doUpdate(org.apache.jcs.engine.behavior.ICacheElement)
413      */
414     @Override
415     protected void processUpdate( ICacheElement<K, V> element )
416     {
417         if ( !alive )
418         {
419             if ( log.isDebugEnabled() )
420             {
421                 log.debug( logCacheName + "No longer alive; aborting put of key = " + element.getKey() );
422             }
423             return;
424         }
425 
426         int[] old = null;
427 
428         // make sure this only locks for one particular cache region
429         storageLock.writeLock().lock();
430 
431         try
432         {
433             old = this.keyStore.get( element.getKey() );
434 
435             if ( old != null )
436             {
437                 this.dataFile.freeBlocks( old );
438             }
439 
440             int[] blocks = this.dataFile.write( element );
441 
442             this.keyStore.put( element.getKey(), blocks );
443 
444             if ( log.isDebugEnabled() )
445             {
446                 log.debug( logCacheName + "Put to file [" + fileName + "] key [" + element.getKey() + "]" );
447             }
448         }
449         catch ( IOException e )
450         {
451             log.error( logCacheName + "Failure updating element, key: " + element.getKey() + " old: " + Arrays.toString(old), e );
452         }
453         finally
454         {
455             storageLock.writeLock().unlock();
456         }
457 
458         if ( log.isDebugEnabled() )
459         {
460             log.debug( logCacheName + "Storing element on disk, key: " + element.getKey() );
461         }
462     }
463 
464     /**
465      * Returns true if the removal was successful; or false if there is nothing to remove. Current
466      * implementation always result in a disk orphan.
467      * <p>
468      * (non-Javadoc)
469      * @param key
470      * @return true if removed anything
471      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doRemove(java.io.Serializable)
472      */
473     @Override
474     protected boolean processRemove( K key )
475     {
476         if ( !alive )
477         {
478             if ( log.isDebugEnabled() )
479             {
480                 log.debug( logCacheName + "No longer alive so returning false for key = " + key );
481             }
482             return false;
483         }
484 
485         boolean reset = false;
486         boolean removed = false;
487 
488         storageLock.writeLock().lock();
489 
490         try
491         {
492             if ( key instanceof String && key.toString().endsWith( CacheConstants.NAME_COMPONENT_DELIMITER ) )
493             {
494                 // remove all keys of the same name group.
495                 Iterator<Map.Entry<K, int[]>> iter = this.keyStore.entrySet().iterator();
496 
497                 while ( iter.hasNext() )
498                 {
499                     Map.Entry<K, int[]> entry = iter.next();
500                     K k = entry.getKey();
501 
502                     if ( k instanceof String && k.toString().startsWith( key.toString() ) )
503                     {
504                         int[] ded = this.keyStore.get( key );
505                         this.dataFile.freeBlocks( ded );
506                         iter.remove();
507                         removed = true;
508                         // TODO this needs to update the remove count separately
509                     }
510                 }
511             }
512             else if ( key instanceof GroupAttrName && ((GroupAttrName<?>)key).attrName == null )
513             {
514                 // remove all keys of the same name hierarchy.
515                 Iterator<Map.Entry<K, int[]>> iter = this.keyStore.entrySet().iterator();
516                 while ( iter.hasNext() )
517                 {
518                     Map.Entry<K, int[]> entry = iter.next();
519                     K k = entry.getKey();
520 
521                     if ( k instanceof GroupAttrName &&
522                         ((GroupAttrName<?>)k).groupId.equals(((GroupAttrName<?>)key).groupId))
523                     {
524                         int[] ded = this.keyStore.get( key );
525                         this.dataFile.freeBlocks( ded );
526                         iter.remove();
527                         removed = true;
528                     }
529                 }
530             }
531             else
532             {
533                 // remove single item.
534                 int[] ded = this.keyStore.remove( key );
535                 removed = ( ded != null );
536                 if ( ded != null )
537                 {
538                     this.dataFile.freeBlocks( ded );
539                 }
540 
541                 if ( log.isDebugEnabled() )
542                 {
543                     log.debug( logCacheName + "Disk removal: Removed from key hash, key [" + key + "] removed = "
544                         + removed );
545                 }
546             }
547         }
548         catch ( Exception e )
549         {
550             log.error( logCacheName + "Problem removing element.", e );
551             reset = true;
552         }
553         finally
554         {
555             storageLock.writeLock().unlock();
556         }
557 
558         if ( reset )
559         {
560             reset();
561         }
562 
563         return removed;
564     }
565 
566     /**
567      * Resets the keyfile, the disk file, and the memory key map.
568      * <p>
569      * (non-Javadoc)
570      * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doRemoveAll()
571      */
572     @Override
573     protected void processRemoveAll()
574     {
575         reset();
576     }
577 
578     /**
579      * Dispose of the disk cache in a background thread. Joins against this thread to put a cap on
580      * the disposal time.
581      * <p>
582      * TODO make dispose window configurable.
583      */
584     @Override
585     public void processDispose()
586     {
587         Runnable disR = new Runnable()
588         {
589             public void run()
590             {
591                 try
592                 {
593                     disposeInternal();
594                 }
595                 catch ( InterruptedException e )
596                 {
597                     log.warn( "Interrupted while diposing." );
598                 }
599             }
600         };
601         Thread t = new Thread( disR, "BlockDiskCache-DisposalThread" );
602         t.start();
603         // wait up to 60 seconds for dispose and then quit if not done.
604         try
605         {
606             t.join( 60 * 1000 );
607         }
608         catch ( InterruptedException ex )
609         {
610             log.error( logCacheName + "Interrupted while waiting for disposal thread to finish.", ex );
611         }
612     }
613 
614     /**
615      * Internal method that handles the disposal.
616      * @throws InterruptedException
617      */
618     protected void disposeInternal()
619         throws InterruptedException
620     {
621         if ( !alive )
622         {
623             log.error( logCacheName + "Not alive and dispose was called, filename: " + fileName );
624             return;
625         }
626         storageLock.writeLock().lock();
627         try
628         {
629             // Prevents any interaction with the cache while we're shutting down.
630             alive = false;
631 
632             this.keyStore.saveKeys();
633 
634             try
635             {
636                 if ( log.isDebugEnabled() )
637                 {
638                     log.debug( logCacheName + "Closing files, base filename: " + fileName );
639                 }
640                 dataFile.close();
641                 // dataFile = null;
642 
643                 // TOD make a close
644                 // keyFile.close();
645                 // keyFile = null;
646             }
647             catch ( IOException e )
648             {
649                 log.error( logCacheName + "Failure closing files in dispose, filename: " + fileName, e );
650             }
651         }
652         finally
653         {
654             storageLock.writeLock().unlock();
655         }
656 
657         if ( log.isInfoEnabled() )
658         {
659             log.info( logCacheName + "Shutdown complete." );
660         }
661     }
662 
663     /**
664      * Returns the attributes.
665      * <p>
666      * (non-Javadoc)
667      * @see org.apache.jcs.auxiliary.AuxiliaryCache#getAuxiliaryCacheAttributes()
668      */
669     public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes()
670     {
671         return this.blockDiskCacheAttributes;
672     }
673 
674     /**
675      * Reset effectively clears the disk cache, creating new files, recyclebins, and keymaps.
676      * <p>
677      * It can be used to handle errors by last resort, force content update, or removeall.
678      */
679     private void reset()
680     {
681         if ( log.isWarnEnabled() )
682         {
683             log.warn( logCacheName + "Resetting cache" );
684         }
685 
686         try
687         {
688             storageLock.writeLock().lock();
689 
690             this.keyStore.reset();
691 
692             if ( dataFile != null )
693             {
694                 dataFile.reset();
695             }
696         }
697         catch ( IOException e )
698         {
699             log.error( logCacheName + "Failure resetting state", e );
700         }
701         finally
702         {
703             storageLock.writeLock().unlock();
704         }
705     }
706 
707     /**
708      * Add these blocks to the emptyBlock list.
709      * <p>
710      * @param blocksToFree
711      */
712     protected void freeBlocks( int[] blocksToFree )
713     {
714         this.dataFile.freeBlocks( blocksToFree );
715     }
716 
717     /**
718      * Gets basic stats for the disk cache.
719      * <p>
720      * @return String
721      */
722     @Override
723     public String getStats()
724     {
725         return getStatistics().toString();
726     }
727 
728     /**
729      * Returns info about the disk cache.
730      * <p>
731      * (non-Javadoc)
732      * @see org.apache.jcs.auxiliary.AuxiliaryCache#getStatistics()
733      */
734     @Override
735     public IStats getStatistics()
736     {
737         IStats stats = new Stats();
738         stats.setTypeName( "Block Disk Cache" );
739 
740         ArrayList<IStatElement> elems = new ArrayList<IStatElement>();
741 
742         IStatElement se = null;
743 
744         se = new StatElement();
745         se.setName( "Is Alive" );
746         se.setData( "" + alive );
747         elems.add( se );
748 
749         se = new StatElement();
750         se.setName( "Key Map Size" );
751         se.setData( "" + this.keyStore.size() );
752         elems.add( se );
753 
754         try
755         {
756             se = new StatElement();
757             se.setName( "Data File Length" );
758             if ( this.dataFile != null )
759             {
760                 se.setData( "" + this.dataFile.length() );
761             }
762             else
763             {
764                 se.setData( "-1" );
765             }
766             elems.add( se );
767         }
768         catch ( Exception e )
769         {
770             log.error( e );
771         }
772 
773         se = new StatElement();
774         se.setName( "Block Size Bytes" );
775         se.setData( "" + this.dataFile.getBlockSizeBytes() );
776         elems.add( se );
777 
778         se = new StatElement();
779         se.setName( "Number Of Blocks" );
780         se.setData( "" + this.dataFile.getNumberOfBlocks() );
781         elems.add( se );
782 
783         se = new StatElement();
784         se.setName( "Average Put Size Bytes" );
785         se.setData( "" + this.dataFile.getAveragePutSizeBytes() );
786         elems.add( se );
787 
788         se = new StatElement();
789         se.setName( "Empty Blocks" );
790         se.setData( "" + this.dataFile.getEmptyBlocks() );
791         elems.add( se );
792 
793         // get the stats from the super too
794         // get as array, convert to list, add list to our outer list
795         IStats sStats = super.getStatistics();
796         IStatElement[] sSEs = sStats.getStatElements();
797         List<IStatElement> sL = Arrays.asList( sSEs );
798         elems.addAll( sL );
799 
800         // get an array and put them in the Stats object
801         IStatElement[] ses = elems.toArray( new StatElement[0] );
802         stats.setStatElements( ses );
803 
804         return stats;
805     }
806 
807     /**
808      * This is used by the event logging.
809      * <p>
810      * @return the location of the disk, either path or ip.
811      */
812     @Override
813     protected String getDiskLocation()
814     {
815         return dataFile.getFilePath();
816     }
817 }