All Downloads are FREE. Search and download functionalities are using the official Maven repository.

geotrellis.process.cache.scala Maven / Gradle / Ivy

The newest version!
package geotrellis.process

import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import scala.collection.mutable.HashMap
import scala.collection.mutable.ListBuffer
import scala.collection.mutable

/** 
 * Trait for a T-keyed, any valued cache.
 */
trait Cache[T] extends Serializable {
  /** Lookup the value for key k
   * @return Some(v) if the value was cached, None otherwise
   */
  def lookup[V](k: T):Option[V]

  /** Insert the value v (keyed by k) into the cache
   * @return true if the value was inserted into the cache
   */
  def insert[V](k: T, v: V):Boolean

  /** Remove k from the cache */
  def remove[V](k: T):Option[V]

  /** Lookup the value for key k
   * If the value is in the cache, return it.
   * Otherwise, insert the value v and return it
   *
   * Note: This method does not guarantee that v will be in the cache when it returns
   *
   * @return the cached value keyed to k or the computed value of v
   */
  def getOrInsert[V](k: T, vv: => V):V = lookup(k) match {
    case Some(v) => v
    case None => {insert(k, vv); vv}
  }
}

/** 
 * Simple HashMap backed cache keyed by String and can hold any type.
 */
class HashCache[T] extends Cache[T] {
  val cache = new mutable.HashMap[T,Any].empty

  def lookup[V](k: T):Option[V] = 
    cache.get(k) match {
      case Some(v) => Some(v.asInstanceOf[V])
      case None => None
    }

  def remove[V](k: T):Option[V] = 
    cache.remove(k) match {
      case Some(v) => Some(v.asInstanceOf[V])
      case None => None
    }

  def insert[V](k: T, v: V):Boolean = { cache.put(k,v.asInstanceOf[Any]); true }
}

/** Base trait for a caching strategy
 *
 * K is the cache key
 * V is the cache value
 */
trait CacheStrategy[K,V] {

  /** Lookup the value for key k
   * @return Some(v) if the value was cached, None otherwise
   */
  def lookup(k: K):Option[V]

  /** Insert the value v (keyed by k) into the cache
   * @return true if the value was inserted into the cache
   */
  def insert(k: K, v: V):Boolean

  /** Remove k from the cache */
  def remove(k: K):Option[V]

  /** Lookup the value for key k
   * If the value is in the cache, return it.
   * Otherwise, insert the value v and return it
   *
   * Note: This method does not guarantee that v will be in the cache when it returns
   *
   * @return the cached value keyed to k or the computed value of v
   */
  def getOrInsert(k: K, vv: => V):V = lookup(k) match {
    case Some(v) => v
    case None => {insert(k, vv); vv}
  }

}

/** A Cache Strategy that completely ignores caching and always returns the input object
 * Operations on this cache execute in O(1) time
 */
class NoCacheStrategy[K,V] extends CacheStrategy[K,V] {
  def lookup(k: K):Option[V] = None
  def insert(k: K, v: V):Boolean = false
  def remove(k: K):Option[V] = None
}

/** An unbounded hash-backed cache
 * Operations on this cache execute in O(1) time
 */
trait HashBackedCache[K,V] extends CacheStrategy[K,V] {
  val cache = new HashMap[K,V].empty

  def lookup(k: K):Option[V] = cache.get(k)
  def remove(k: K):Option[V] = cache.remove(k)
  def insert(k: K, v: V):Boolean = { cache.put(k,v); true }
}

trait LoggingCache[K,V] extends CacheStrategy[K,V] {
  abstract override def lookup(k: K):Option[V] = super.lookup(k) match {
    case None => { 
      //println("Cache miss on %s".format(k));
      None 
    }
    case z => { 
      //println("Cache hit on %s".format(k));
      z
    }
  }
}

/** A hash backed cache with a size boundary
 * Operations on this cache may required O(N) time to execute (N = size of cache)
 */
trait BoundedCache[K,V] extends CacheStrategy[K,V] {
  
  /** Return the size of a a given cache item
   * If items are objects (for example) this might just be: (x) => 1
   * If items are arrays of ints and the maxSize is in bytes this function might be:
   *   (x) => x.length * 4
   */
  val sizeOf: V => Long

  /** Maximum size of the cache
   */
  val maxSize: Long

  /** Attempt to free l amount of cache space
   * this method is called once after an insert would have failed due to
   * space constraints. After this method returns the insert will be retried
   */
  def cacheFree(l: Long):Unit

  var currentSize: Long = 0

  /**
   * returns true iff v can be inserted into the cache without violating the
   * size boundary
   */
  def canInsert(v: V) = {
    val ok = currentSize + sizeOf(v) <= maxSize
    //println("[cache] canInsert says %s (%d + %d <= %d)" format (ok, currentSize, sizeOf(v), maxSize))
    ok
  }

  abstract override def remove(k: K):Option[V] = lookup(k) match {
    case None => None
    case Some(v) => {
      currentSize -= sizeOf(v)
      super.remove(k)
    }
  }

  abstract override def insert(k: K, v: V):Boolean = {
    // If something is already at this location, remove it from the cache size
    lookup(k) map ( r => currentSize -= sizeOf(r) )

    if (!canInsert(v)) {
      cacheFree(currentSize + sizeOf(v) - maxSize)
    } 

    if (canInsert(v)) {
      currentSize += sizeOf(v)
      //println("[Cache] Inserted %s".format(k))
      super.insert(k,v)

      true
    } else {
      //println("[Cache] Failed to insert %s (cache full)".format(k))
      false
    }
  }
}

trait OrderedBoundedCache[K,V] extends BoundedCache[K,V] {

  /** Called when attempt to make space in the cache. This function should
   * return the index of the item to remove next
   * 
   * @param k: Seq[K]  first element was accessed most recently
   */
  val removeIdx: Seq[K] => Int

  /** Attempt to free space in the cache starting with the last accessed element
   * @param ltgt: The additional space in the cache requested
   */
  def cacheFree(ltgt: Long):Unit = {
    //println("[Cache] Attempting to free %d units of data (cache max: %d, cache cur: %d)".format(ltgt, maxSize, currentSize))

    // if (ltgt > maxSize) {
    //    println("[Cache] File to big to fit in cache at all")
    // }

    var l = ltgt
    while(l > 0 && cacheOrder.length > 0) {
      val item:K = cacheOrder.remove(removeIdx(cacheOrder))
      val removedSize: Long = remove(item) match {
        case Some(v) => {
//          println("[Cache]\tEvicted %s (%d units) from cache".format(item, sizeOf(v)))
          sizeOf(v)
        }
        case None => 0L
      }
      l -= removedSize
    }
  }

  /* Contains order of cache requests
   * the item at index 0 was read most recently and item at (length - 1) was read the longest ago
   */
  private[this] var cacheOrder:ListBuffer[K] = new ListBuffer[K]()
  private[this] var lock = new Object();

  // Signal that the value for k was recently looked up
  private[this] def prepend(k: K) = {
    lock.synchronized {
      cacheOrder -= k
      cacheOrder.prepend(k)
    }
    k
  }

  abstract override def lookup(k: K) = super.lookup(k) match {
    case v@Some(_) => { prepend(k); v }
    case None => None
  }

  abstract override def insert(k: K, v: V) = if (super.insert(k,v)) {
    prepend(k); true 
  } else {
    false
  }
}        

class LRUCache[K,V](val maxSize: Long, val sizeOf: V => Long = (v:V) => 1) extends HashBackedCache[K,V] with OrderedBoundedCache[K,V] with AtomicCache[K,V] with LoggingCache[K,V] {
  val removeIdx: Seq[K] => Int = (s: Seq[K]) => s.length - 1
}

class MRUCache[K,V](val maxSize: Long, val sizeOf: V => Long = (v:V) => 1) extends HashBackedCache[K,V] with OrderedBoundedCache[K,V] with AtomicCache[K,V] with LoggingCache[K,V] {
  val removeIdx: Seq[K] => Int = (s: Seq[K]) => 0
}


/** Atomic cache provides an atomic getOrInsert(k,v) method
 * This cache assumes that (k,v) pair is immutable
 */
trait AtomicCache[K,V] extends CacheStrategy[K,V] {
  val bigLock:Lock = new ReentrantLock()

  val currentlyLoading:HashMap[K,Lock] = new HashMap[K,Lock].empty

  abstract override def lookup(k: K):Option[V] = {
    bigLock.lock()
    val smallLockOpt = currentlyLoading.get(k)
    bigLock.unlock()

    smallLockOpt.map(smallLock => {
      smallLock.lock()
      smallLock.unlock()
    })

    super.lookup(k)
  }

  abstract override def getOrInsert(k: K, v: => V):V = {
    val t0 = System.currentTimeMillis
    bigLock.lock()
    try {
      if (currentlyLoading.contains(k)) {
        // Another thread is currently loading up the cache entry for k so we
        // block until it is done.
        val smallLock = currentlyLoading.get(k).get
        bigLock.unlock()

        // the small lock blocks until the thread that is doing the loading is
        // complete.
        smallLock.lock()
        smallLock.unlock()
        
        // v will already have been evaluated by some other thread
        // if there was an error we'll evaluate v
        //super.lookup(k).getOrElse(v)
        val resultOpt = super.lookup(k)
        resultOpt match {
          case None => {
            val vv = v
            val t = System.currentTimeMillis - t0            
            //println("waited on other thread, but failed: %d ms" format (t))
            vv
          }
          case Some(vv) => {
            val t = System.currentTimeMillis - t0            
            //println("waited on other thread: %d ms" format (t))
            vv
          }
        }
      } else {
        super.lookup(k) match {
          case Some(vv) => {
            bigLock.unlock()
            val t = System.currentTimeMillis - t0            
            //println("found in cache: %d ms" format t)
            vv
          }
          case None => {
            val smallLock = new ReentrantLock()
            currentlyLoading.put(k, smallLock)
            smallLock.lock()
            val vv = try {
              bigLock.unlock()
            
              val vv = v    // Evaluate v

              bigLock.lock()
              super.insert(k,v)

              currentlyLoading.remove(k)
              bigLock.unlock()
              vv

            } catch {
              case t:Throwable => { println("error"); t.printStackTrace(); println("rethrow..."); throw t }
            } finally {
              smallLock.unlock()
            }
            
            val t = System.currentTimeMillis - t0            
            //println("added to cache: %d ms" format t)
            vv
          }
        }
      }
    } catch {
      case t:Throwable => {
        bigLock.unlock()
        throw t
      }
    }
  }
      

  abstract override def insert(k: K, v: V):Boolean = {
    bigLock.lock()
    try {
      super.insert(k,v)
    } finally {
      bigLock.unlock()
    }
  }          
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy