You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
96 lines
2.9 KiB
96 lines
2.9 KiB
package com.xsn.explorer.cache
|
|
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
import com.alexitc.playsonify.core.FutureApplicationResult
|
|
import com.alexitc.playsonify.core.FutureOr.Implicits.FutureOps
|
|
import com.alexitc.playsonify.models.ordering.OrderingCondition
|
|
import com.alexitc.playsonify.models.pagination.Limit
|
|
import com.github.benmanes.caffeine.cache.{Cache, Caffeine}
|
|
import com.xsn.explorer.models.WrappedResult
|
|
import com.xsn.explorer.models.persisted.BlockHeader
|
|
import com.xsn.explorer.models.values.Blockhash
|
|
import org.scalactic.Good
|
|
import play.api.libs.json.{JsValue, Json, Writes}
|
|
|
|
import scala.concurrent.{ExecutionContext, Future}
|
|
|
|
/**
|
|
* This cache aims to provide fast access to the block headers data used for syncing the light wallet.
|
|
*
|
|
* The ideal strategy is to cache an entry and never expire it because most headers are unlikely to change.
|
|
*
|
|
* In order to handle chain reorganizations, the last entry must not be cached (TODO).
|
|
*
|
|
* Currently, the entries are expired after some minutes just to keep the last entry updated.
|
|
*
|
|
* Also, it is faster to cache the JSON data that is sent in a response instead of being encoding it on each request.
|
|
*/
|
|
class BlockHeaderCache(cache: Cache[BlockHeaderCache.Key, BlockHeaderCache.EncodedValue]) {
|
|
|
|
import BlockHeaderCache._
|
|
|
|
def getOrSet(
|
|
key: Key,
|
|
entrySize: Int)(
|
|
f: => FutureApplicationResult[Value])(
|
|
implicit ec: ExecutionContext,
|
|
writes: Writes[Value]): FutureApplicationResult[EncodedValue] = {
|
|
|
|
if (isCacheable(key, entrySize)) {
|
|
get(key)
|
|
.map(v => Future.successful(Good(v)))
|
|
.getOrElse {
|
|
val result = for {
|
|
r <- f.toFutureOr
|
|
} yield (r.data.size, Json.toJson(r))
|
|
|
|
// set cache only if the response is complete
|
|
val _ = result.map { case (size, json) =>
|
|
if (size == key.limit.int) {
|
|
cache.put(key, json)
|
|
}
|
|
}
|
|
|
|
result.map(_._2).toFuture
|
|
}
|
|
} else {
|
|
val result = for {
|
|
r <- f.toFutureOr
|
|
} yield Json.toJson(r)
|
|
|
|
result.toFuture
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A block header list is cacheable if all the following meet:
|
|
* - The ordering is from oldest to newest
|
|
* - The limit matches the expected entry size
|
|
* - The entry is not the last one (TODO)
|
|
*/
|
|
def isCacheable(key: Key, entrySize: Int): Boolean = {
|
|
key.orderingCondition == OrderingCondition.AscendingOrder &&
|
|
key.limit.int == entrySize
|
|
}
|
|
|
|
def get(key: Key): Option[EncodedValue] = {
|
|
Option(cache.getIfPresent(key))
|
|
}
|
|
}
|
|
|
|
object BlockHeaderCache {
|
|
|
|
case class Key(limit: Limit, orderingCondition: OrderingCondition, lastSeenHash: Option[Blockhash])
|
|
|
|
type Value = WrappedResult[List[BlockHeader]]
|
|
type EncodedValue = JsValue
|
|
|
|
def default: BlockHeaderCache = {
|
|
val cache = Caffeine.newBuilder()
|
|
.expireAfterWrite(30, TimeUnit.MINUTES)
|
|
.build[Key, EncodedValue]
|
|
|
|
new BlockHeaderCache(cache)
|
|
}
|
|
}
|
|
|