Changeset 4827


Ignore:
Timestamp:
Mar 23, 2009, 1:09:35 PM (13 years ago)
Author:
Nicklas Nordborg
Message:

References #1261: Global cache for static data

The cache is now thread-safe. Configuration options in base.config are supported. Old files are cleaned up. Unless something shows up this should now be fully functional.

Location:
trunk
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/config/dist/base.config

    r4637 r4827  
    155155cache.timeout = 20
    156156
     157# If the static cache should be disabled or enabled
     158# Disabling the static cache may reduce performance for
     159# certain operations
     160cache.static.disabled = false
     161
     162# Timeout (in days) for items in the static cache
     163# Items that hasn't been accessed in the configured amount of
     164# of time will be removed from the cache
     165cache.static.max-age = 30
     166
    157167#Overwrite the existing help texts when updating the program
    158168helptext.update = true
  • trunk/doc/src/docbook/appendix/base.config.xml

    r4637 r4827  
    592592      </listitem>
    593593    </varlistentry>
     594   
     595    <varlistentry>
     596      <term><property>cache.static.disabled</property></term>
     597      <listitem>
     598        <para>
     599        If the static cache should be enabled or disabled. It is enabled by
     600        default. Disabling the static cache may reduce performance in some
     601        cases. The static cache is used to cache processed information,
     602        for example images, so that the database doesn't have to be queried
     603        on every request.
     604          </para>
     605      </listitem>
     606    </varlistentry>
     607   
     608    <varlistentry>
     609      <term><property>cache.static.max-age</property></term>
     610      <listitem>
     611        <para>
     612        The maximum age in days of files in the static cache. Files that
     613        hasn't been accessed (read or written) in the specified amount
     614        of time are deleted.
     615          </para>
     616      </listitem>
     617    </varlistentry>
     618   
    594619    <varlistentry>
    595620      <term><property>helptext.update</property></term>
  • trunk/src/core/net/sf/basedb/core/Application.java

    r4824 r4827  
    449449      // Initialise other utility classes
    450450      staticCache = new StaticCache(new java.io.File(userFilesDirectory, "static.cache"));
     451      staticCache.setDisabled(true);
    451452      QueryExecutor.init();
    452453      HibernateUtil.init1();
     
    501502        RawDataTypes.initPlatforms();
    502503 
    503         // Adding a task that cleans the session control cache at regular intervale
     504        // Adding a task that cleans the session control cache at regular intervals
    504505        long milliSeconds = 60 * 1000 * sessionCacheTimeout;
    505506        getScheduler().schedule(new SessionControlCacheCleaner(), milliSeconds, milliSeconds, false);
     507       
     508        // Adding a task that cleans the static cache at regular intervals
     509        staticCache.setDisabled(Config.getBoolean("cache.static.disabled"));
     510        if (!staticCache.isDisabled())
     511        {
     512          long maxFileAge = 24L * 3600L * 1000L * Config.getLong("cache.static.max-age", 30);
     513          long checkInterval = 12 * 3600 * 1000; // Once every twelve hours
     514          getScheduler().schedule(
     515            staticCache.cleanUpTask(staticCache.olderThan(maxFileAge)), 60000, checkInterval, false);
     516        }
    506517       
    507518        if (useInternalJobQueue == null) useInternalJobQueue = Config.getBoolean("jobqueue.internal.enabled");
  • trunk/src/core/net/sf/basedb/util/StaticCache.java

    r4826 r4827  
    2323
    2424import java.io.File;
     25import java.io.FileFilter;
    2526import java.io.FileInputStream;
    2627import java.io.FileOutputStream;
     28import java.io.FilterInputStream;
     29import java.io.FilterOutputStream;
    2730import java.io.IOException;
    2831import java.io.InputStream;
     
    3134import java.io.OutputStream;
    3235import java.io.Serializable;
     36import java.lang.ref.WeakReference;
     37import java.util.List;
     38import java.util.Map;
     39import java.util.TimerTask;
     40import java.util.WeakHashMap;
     41import java.util.concurrent.TimeUnit;
     42import java.util.concurrent.locks.Lock;
     43import java.util.concurrent.locks.ReadWriteLock;
     44import java.util.concurrent.locks.ReentrantReadWriteLock;
     45import java.util.regex.Pattern;
     46
     47import net.sf.basedb.core.InvalidDataException;
    3348
    3449/**
     
    3651  use case is to store data this is expensive to get from the database
    3752  in a file for later retrieval. The cache can be used in streaming mode
    38   with the any of the {@link #read(String)} or {@link #write(String, InputStream)}
     53  with the any of the {@link #read(String, int)} or {@link #write(String, InputStream, int)}
    3954  methods and their variants. It can also be used to store any {@link Serializable}
    40   object with {@link #store(String, Serializable)} and {@link #load(String)}.
     55  object with {@link #store(String, Serializable, int)} and {@link #load(String, int)}.
    4156  <p>
     57 
    4258  In all cases the cached entry is identified by a key which is more or
    4359  less directly translated to directories on the file system.
    4460  <p>
    4561 
     62  This class is thread-safe and can be used by multiple threads at the same
     63  time. Write requests to the same entry in the cache are allowed to one
     64  thread at a time. Any number of threads may read from the same cache entry
     65  as long as no thread is writing to that entry.
    4666 
    4767  @author Nicklas
     
    5171public class StaticCache
    5272{
     73
     74  public static final Pattern validKey = Pattern.compile("[\\w\\/\\.\\-]+");
     75
     76  /**
     77    Log static cache events.
     78  */
     79  private static final org.apache.log4j.Logger log =
     80    org.apache.log4j.LogManager.getLogger("net.sf.basedb.util.StaticCache");
     81
     82  /**
     83    Checks if the given key is a vilid cache entry key.
     84    Allowed characters are: any alphanumerical character + ./-
     85    @param key The key to check
     86    @return TRUE if the key is valid
     87  */
     88  public static boolean isValidKey(String key)
     89  {
     90    return validKey.matcher(key).matches();
     91  }
     92 
    5393  private final File root;
     94  private final Map<String, LockEntry> locks;
    5495  private boolean disabled;
     96 
    5597 
    5698  /**
     
    61103  {
    62104    this.root = root;
     105    this.locks = new WeakHashMap<String, LockEntry>();
     106    log.info("Creating static cache in directory " + root);
    63107  }
    64108
     
    79123  public void setDisabled(boolean disabled)
    80124  {
     125    if (disabled)
     126    {
     127      log.info("Disabling static cache in directory " + root);
     128    }
     129    else
     130    {
     131      log.info("Enabling static cache in directory " + root);
     132    }
    81133    this.disabled = disabled;
    82134  }
    83135 
    84136  /**
    85     Store information in the cache. If the entry already exists,
     137    Remove all files that matches the specified filter from
     138    the cache. This method is synchronized and can only
     139    be executed by one thread at a time.
     140   
     141    @param filter A file filter that matches the files to
     142      remove
     143  */
     144  public synchronized void cleanUp(FileFilter filter)
     145  {
     146    log.info("Cleaning up static cache: " + root);
     147    List<File> oldFiles = FileUtil.findFiles(root, filter);
     148    if (oldFiles == null) return;
     149   
     150    log.debug("Found " + oldFiles.size() + " files that should be deleted");
     151    int numDeleted = 0;
     152    for (File f : oldFiles)
     153    {
     154      if (f.delete())
     155      {
     156        log.debug("Removed cached file: " + f);
     157        numDeleted++;
     158      }
     159      else
     160      {
     161        log.warn("Failed to remove cached file: " + f);
     162      }
     163    }
     164    log.info("Removed " + numDeleted + " files from the static cache: " + root);
     165  }
     166 
     167  /**
     168    Creates a file filter that matches all files that
     169    are older than the specified age.
     170    @param age The age in milliseconds
     171    @return A file filter
     172    @see OlderThanFileFilter
     173  */
     174  public FileFilter olderThan(long age)
     175  {
     176    return new OlderThanFileFilter(age, true);
     177  }
     178 
     179  /**
     180    Creates a task that cleans up this cache when it is
     181    executed.
     182    @param filter The file filter that determines
     183      which file that should be deleted
     184    @return A TimerTask object
     185    @see #cleanUp(FileFilter)
     186  */
     187  public TimerTask cleanUpTask(FileFilter filter)
     188  {
     189    return new CleanupTask(this, filter);
     190  }
     191 
     192  /**
     193    Store binary information in the cache. If the entry already exists,
    86194    it is overwritten. If the entry is locked by other threads,
    87     this thread wai
     195    this thread waits the specified number of milliseconds before
     196    returning.
    88197   
    89198    @param key The cache key
    90199    @param in An input stream to read from
     200    @param timeout A timeout in milliseconds to wait for a write lock
     201      on the requested cache entry
    91202    @return The number of bytes written
    92203  */
    93   public long write(String key, InputStream in)
     204  public long write(String key, InputStream in, int timeout)
    94205    throws IOException
    95206  {
    96207    if (disabled) return 0;
    97     OutputStream out = getOutputStream(key);
    98     long numBytes = FileUtil.copy(in, out);
    99     out.flush();
    100     out.close();
     208    long numBytes = 0;
     209    OutputStream out = null;
     210    try
     211    {
     212      out = getOutputStream(key, timeout);
     213      if (out != null)
     214      {
     215        numBytes = FileUtil.copy(in, out);
     216        out.flush();
     217      }
     218    }
     219    finally
     220    {
     221      if (out != null) out.close();
     222    }
    101223    return numBytes;
    102224  }
    103225 
    104   public OutputStream write(String key)
     226  /**
     227    Get an output stream that can be used to write binary information
     228    to the cache. If the entry already exists, it is overwritten.
     229    If the entry is locked by other threads, this thread waits the
     230    specified number of milliseconds before giving up.
     231    <p>
     232    NOTE! It is very important that the caller closes the
     233    output stream as soon as all data has been written to it.
     234    Failure to do so may result in locking the cache entry from
     235    reading by other threads.
     236   
     237    @param key The cache key
     238    @param timeout A timeout in milliseconds to wait for a write lock
     239      on the requested cache entry
     240    @return An output stream, or null if a write lock could not
     241      be aquired
     242  */
     243  public OutputStream write(String key, int timeout)
    105244    throws IOException
    106245  {
    107246    if (disabled) return null;
    108     return getOutputStream(key);
    109   }
    110  
    111  
    112   public long read(String key, OutputStream out)
     247    return getOutputStream(key, timeout);
     248  }
     249 
     250  /**
     251    Read binary information from the cache. The contents of the
     252    specified cache entry will be copied to the specified output
     253    stream.
     254
     255    @param key The cache key
     256    @param out An output stream to write the cache contents to
     257    @param timeout A timeout in milliseconds to wait for a
     258      read lock on the requested cache entry
     259    @return The number of bytes copied
     260    @throws IOException
     261  */
     262  public long read(String key, OutputStream out, int timeout)
    113263    throws IOException
    114264  {
    115265    if (disabled) return 0;
    116     InputStream in = getInputStream(key);
    117266    long numBytes = 0;
    118     if (in != null) FileUtil.copy(in, out);
     267    InputStream in = null;
     268    try
     269    {
     270      in = getInputStream(key, timeout);
     271      if (in != null)
     272      {
     273        numBytes = FileUtil.copy(in, out);
     274      }
     275    }
     276    finally
     277    {
     278      if (in != null) in.close();
     279    }
    119280    return numBytes;
    120281  }
    121282 
    122   public InputStream read(String key)
     283  /**
     284    Get an input stream for reading binary information from the cache.
     285 
     286    @param key The cache key
     287    @param timeout A timeout in milliseconds to wait for a
     288      read lock on the requested cache entry
     289    @return An input stream or null if the cache entry doesn't exists
     290      or if a read lock could not be aquired
     291    @throws IOException
     292  */
     293  public InputStream read(String key, int timeout)
    123294    throws IOException
    124295  {
    125296    if (disabled) return null;
    126     return getInputStream(key);
    127   }
    128  
    129   public boolean store(String key, Serializable object)
     297    return getInputStream(key, timeout);
     298  }
     299 
     300  /**
     301    Store a serializable object in the cache. If the entry already exists,
     302    it is overwritten. If the entry is locked by other threads,
     303    this thread waits the specified number of milliseconds before
     304    returning.
     305   
     306    @param key The cache key
     307    @param object The object to store in the cache
     308    @param timeout A timeout in milliseconds to wait for a write lock
     309      on the requested cache entry
     310    @return TRUE if the object could be stored, FALSE otherwise
     311  */
     312  public boolean store(String key, Serializable object, int timeout)
    130313    throws IOException
    131314  {
    132315    if (disabled) return false;
    133     ObjectOutputStream out = new ObjectOutputStream(getOutputStream(key));
    134     out.writeObject(object);
    135     out.flush();
    136     out.close();
     316    OutputStream out = null;
     317    try
     318    {
     319      out = getOutputStream(key, timeout);
     320      if (out != null)
     321      {
     322        ObjectOutputStream oos = new ObjectOutputStream(out);
     323        oos.writeObject(object);
     324        oos.flush();
     325      }
     326    }
     327    finally
     328    {
     329      if (out != null) out.close();
     330    }
    137331    return true;
    138332  }
    139333 
    140   public Serializable load(String key)
     334  /**
     335    Read a serializable object from the cache.
     336   
     337    @param key The cache key
     338    @param timeout A timeout in milliseconds to wait for a read lock
     339      on the requested cache entry
     340    @return The materialized object, or null if the cache entry doesn't
     341      exists or if a read lock couldn't be aquired
     342  */
     343  public Serializable load(String key, int timeout)
    141344    throws IOException
    142345  {
    143346    if (disabled) return null;
    144     InputStream in = getInputStream(key);
    145347    Serializable object = null;
    146     if (in != null)
    147     {
    148       ObjectInputStream oin = new ObjectInputStream(in);
    149       try
    150       {
    151         object = (Serializable)oin.readObject();
    152       }
    153       catch (ClassNotFoundException ex)
    154       {
    155         throw new IOException(ex);
    156       }
     348    InputStream in = null;
     349    try
     350    {
     351      in = getInputStream(key, timeout);
     352      if (in != null)
     353      {
     354        ObjectInputStream oin = new ObjectInputStream(in);
     355        try
     356        {
     357          object = (Serializable)oin.readObject();
     358        }
     359        catch (ClassNotFoundException ex)
     360        {
     361          throw new IOException(ex);
     362        }
     363      }
     364    }
     365    finally
     366    {
     367      if (in != null) in.close();
    157368    }
    158369    return object;   
     
    161372  private void validateKey(String key)
    162373  {
    163    
    164   }
    165  
    166   private InputStream getInputStream(String key)
     374    if (!isValidKey(key))
     375    {
     376      throw new InvalidDataException("Invalid cache key: " + key);
     377    }
     378  }
     379 
     380  /**
     381    Get a lock-safe input stream.
     382  */
     383  private InputStream getInputStream(String key, int timeout)
    167384    throws IOException
    168385  {
    169386    validateKey(key);
     387    log.debug("Read request for static cache: " + key);
    170388    File f = new File(root, key);
    171     if (!f.exists()) return null;
    172     return new FileInputStream(f);
    173   }
    174 
    175   private OutputStream getOutputStream(String key)
     389    if (!f.exists())
     390    {
     391      log.debug("Cache entry doesn't exist: " + key);
     392      return null;
     393    }
     394    LockEntry lock = aquireLock(key, false, timeout);
     395    if (lock == null) return null;
     396    InputStream in = null;
     397    try
     398    {
     399      f.setLastModified(System.currentTimeMillis());
     400      in = new LockSafeInputStream(new FileInputStream(f), lock);
     401    }
     402    finally
     403    {
     404      if (in == null) lock.readLock().unlock();
     405    }
     406    return in;
     407  }
     408
     409  /**
     410    Get a lock-safe output stream.
     411  */
     412  private OutputStream getOutputStream(String key, int timeout)
    176413    throws IOException
    177414  {
    178415    validateKey(key);
    179     File f = new File(root, key);
    180     f.getParentFile().mkdirs();
    181     f.createNewFile();
    182     return new FileOutputStream(f);
    183   }
    184 
    185  
     416    log.debug("Write request for static cache: " + key);
     417    LockEntry lock = aquireLock(key, true, timeout);
     418    if (lock == null) return null;
     419    OutputStream out = null;
     420    try
     421    {
     422      File f = new File(root, key);
     423      f.getParentFile().mkdirs();
     424      f.createNewFile();
     425      out = new LockSafeOutputStream(new FileOutputStream(f), lock);
     426    }
     427    finally
     428    {
     429      if (out == null) lock.writeLock().unlock();
     430    }
     431    return out;
     432  }
     433
     434  /**
     435    Aquire a read or write lock on a given cache entry.
     436    @return A lock manager or null if the lock could not be aquired
     437  */
     438  private LockEntry aquireLock(String key, boolean writeLock, int timeout)
     439  {
     440    String lockType = writeLock ? "write" : "read";
     441    log.debug("Trying to get " + lockType + " lock on: " + key);
     442    LockEntry rwLock = null;
     443    synchronized (locks)
     444    {
     445      log.debug("Number of known lock entries: " + locks.size());
     446      rwLock = locks.get(key);
     447      if (rwLock == null)
     448      {
     449        log.debug("Create new lock holder for: " + key);
     450        rwLock = new LockEntry(new ReentrantReadWriteLock(), key);
     451        locks.put(key, rwLock);
     452      }
     453    }
     454   
     455    Lock lock = writeLock ? rwLock.writeLock() : rwLock.readLock();
     456    try
     457    {
     458      if (!lock.tryLock(timeout, TimeUnit.MILLISECONDS))
     459      {
     460        log.debug("Timeout while waiting for " + lockType + " lock on: " + key);
     461        rwLock = null;
     462      }
     463    }
     464    catch (InterruptedException ex)
     465    {
     466      log.debug("Interrupted while waiting for " + lockType + " lock on: " + key);
     467      rwLock = null;
     468    }
     469    if (rwLock != null) log.debug("Got " + lockType + " lock on: " + key);
     470    return rwLock;
     471  }
     472 
     473  /**
     474    A lock-safe output stream that releases the associated
     475    write lock when the stream is closed.
     476    <p>
     477    NOTE! It is important that we keep a strong reference
     478    to the 'key' in this class until the stream is closed,
     479    since otherwise the locked entry may be reclaimed by the
     480    garbage collector.
     481  */
     482  static class LockSafeOutputStream
     483    extends FilterOutputStream
     484  {
     485    private final Throwable calledFrom;
     486    private ReadWriteLock lock;
     487    private String key;
     488    private boolean closed;
     489
     490    LockSafeOutputStream(OutputStream out, LockEntry lock)
     491    {
     492      super(out);
     493      this.lock = lock;
     494      this.key = lock.getKey();
     495      this.calledFrom = new Throwable();
     496    }
     497
     498    @Override
     499    public void close()
     500      throws IOException
     501    {
     502      if (closed) return;
     503      log.debug("Releasing write lock on: " + key);
     504      lock.writeLock().unlock();
     505      closed = true;
     506      lock = null;
     507      key = null;
     508      super.close();
     509    }
     510   
     511    @Override
     512    protected void finalize()
     513      throws Throwable
     514    {
     515      if (!closed)
     516      {
     517        log.warn("Found unreleased write lock on: " + key, calledFrom);
     518        close();
     519      }
     520      super.finalize();
     521    }
     522 
     523    @Override
     524    public String toString()
     525    {
     526      return "LockSafeOutputStream[" + key + "]";
     527    }
     528
     529  }
     530 
     531 
     532  /**
     533    A lock-safe input stream that releases the associated
     534    read lock when the stream is closed.
     535    <p>
     536    NOTE! It is important that we keep a strong reference
     537    to the 'key' in this class until the stream is closed,
     538    since otherwise the locked entry may be reclaimed by the
     539    garbage collector.
     540  */
     541  static class LockSafeInputStream
     542    extends FilterInputStream
     543  {
     544 
     545    private final Throwable calledFrom;
     546    private ReadWriteLock lock;
     547    private String key;
     548    private boolean closed;
     549 
     550    LockSafeInputStream(InputStream in, LockEntry lock)
     551    {
     552      super(in);
     553      this.lock = lock;
     554      this.key = lock.getKey();
     555      this.calledFrom = new Throwable();
     556    }
     557 
     558    @Override
     559    public void close()
     560      throws IOException
     561    {
     562      if (closed) return;
     563      log.debug("Releasing read lock on: " + key);
     564      lock.readLock().unlock();
     565      closed = true;
     566      lock = null;
     567      key = null;
     568      super.close();
     569    }
     570   
     571    @Override
     572    protected void finalize()
     573      throws Throwable
     574    {
     575      if (!closed)
     576      {
     577        log.warn("Found unreleased read lock on: " + key, calledFrom);
     578        close();
     579      }
     580      super.finalize();
     581    }
     582   
     583    @Override
     584    public String toString()
     585    {
     586      return "LockSafeInputStream[" + key + "]";
     587    }
     588  }
     589
     590  /**
     591    Keeps track of a locked cached entry. The 'key' is the same key
     592    instance that was used to create the entry in the first place.
     593    It is important that all active maintainers of a lock also
     594    keeps a strong reference to the key since the lock entry may
     595    otherwise be reclaimed by the garbage collector.
     596  */
     597  static class LockEntry
     598    implements ReadWriteLock
     599  {
     600    private ReadWriteLock rwLock;
     601    private WeakReference<String> keyRef;
     602   
     603    LockEntry(ReadWriteLock rwLock, String key)
     604    {
     605      this.rwLock = rwLock;
     606      this.keyRef = new WeakReference<String>(key);
     607    }
     608   
     609    @Override
     610    public Lock writeLock()
     611    {
     612      return rwLock.writeLock();
     613    }
     614   
     615    @Override
     616    public Lock readLock()
     617    {
     618      return rwLock.readLock();
     619    }
     620   
     621    public String getKey()
     622    {
     623      return keyRef.get();
     624    }
     625   
     626    @Override
     627    public String toString()
     628    {
     629      return "LockEntry[" + getKey() + "]";
     630    }
     631  }
     632 
     633  /**
     634    A timer task that clean up the cache when it is executed.
     635  */
     636  public class CleanupTask
     637    extends TimerTask
     638  {
     639
     640    private final StaticCache cache;
     641    private final FileFilter filter;
     642   
     643    public CleanupTask(StaticCache cache, FileFilter filter)
     644    {
     645      this.cache = cache;
     646      this.filter = filter;
     647    }
     648   
     649    @Override
     650    public void run()
     651    {
     652      cache.cleanUp(filter);
     653    }
     654  }
    186655}
Note: See TracChangeset for help on using the changeset viewer.