Creating memoize for Future values and timeout

Viewed 323

I have the following problem:

I have a API getSimilarTransactions which gets a Transaction:

{
 "name": "zvi",
 "email": "zvi@gmail.com",
 "amount": 100,
...
}

And returns a list of transactions that have similar (name, email) from the persistence database. This operation takes ~40seconds which is a lot of time.

I also know that a lot of the API requests are pretty similar (same id, email) , and they're a lot of API requests per minute.

I want to create memorize caching with the following logic:

  1. If i did already call the database and got a response for (id, email) in the last 30 seconds return this answer
  2. otherwise, make a call to the database and update the cache (call to the database returns Future[List[Transaction]], its also can fail - in that case i do NOT want to update the cache with an error.

I want to solve my problem with memorize, something as follows, but asynchronously with timeout expiration for cache key

def memoize[I,O](f: I => O) = new mutable.HashMap[I, O]() {
    override def apply(x: I) = getOrElseUpdate(x, f(x))
}

Database call:

transactionsDao.getSimilarTransaction(id: String, email: String): Future[List[Transaction]]

Any ideas on how to solve it?

2 Answers

Disclaimer: I wrote this from off the top of my head, YMMV, etc.

Assuming that you have some timer facility, called someTimer in the code below, you should be able to do something equivalent of:

    class Cache[K, V] {
      private final val cached = new java.util.concurrent.ConcurrentHashMap[K, Promise[V]]()
      def get(key: K, orCreate: => Future[V]): Future[V] =
        cached.get(key) match {
          case null =>
            val p = Promise[V]()
            cached.putIfAbsent(key, p) match {
              case null =>
                p.completeWith(orCreate).future.andThen({ case _ => someTimer.after(someTime, () => cached.remove(key, p))})(ExecutionContext.parasitic)
              case existing =>
                existing.future
            }
          case existing =>
            existing.future
        }
    }

Usage:

  import scala.concurrent.ExecutionContext.Implicits.global
  val cache = new Cache[String, Int]

  val f, e = cache.get("foo", Future((scala.math.random() * 1000).toInt))
  f.onComplete(println)
  e.onComplete(println)

Prints:

Success(720)

Success(720)

Take a look at ScalaCache library which has mode for Future: https://cb372.github.io/scalacache/docs/modes.html#future-mode:

import scalacache.modes.scalaFuture._
import scalacache.caffeine.CaffeineCache // in memory cache

implicit val transactionCache: Cache[List[Transaction]] = CaffeineCache[List[Transaction]]

def getTransactions(id: String, email: String) = memoizeF[Future, List[Transaction]](Some(10.seconds)){
    callToDatabase(id, Email)
}

See for more details: https://cb372.github.io/scalacache/docs/memoization.html

Related