refactor mongo caches - closes #5813

pull/5820/head
Thibault Duplessis 2019-12-24 17:56:36 -05:00
parent 9352ebc6fb
commit effe244b0d
21 changed files with 236 additions and 186 deletions

View File

@ -8,7 +8,7 @@ final class Stat(env: Env) extends LilaController(env) {
def ratingDistribution(perfKey: lila.rating.Perf.Key) = Open { implicit ctx =>
lila.rating.PerfType(perfKey).filter(lila.rating.PerfType.leaderboardable.has) match {
case Some(perfType) =>
env.user.cached.ratingDistribution(perfType) map { data =>
env.user.rankingApi.weeklyRatingDistribution(perfType) dmap { data =>
Ok(html.stat.ratingDistribution(perfType, data))
}
case _ => notFound

View File

@ -227,12 +227,11 @@ final class User(
}
def list = Open { implicit ctx =>
val nb = 10
val leaderboards = env.user.cached.top10.get
negotiate(
html =
for {
nbAllTime <- env.user.cached topNbGame nb
nbAllTime <- env.user.cached.top10NbGame.get({})
tourneyWinners <- env.tournament.winners.all.map(_.top)
_ <- env.user.lightUserApi preloadMany tourneyWinners.map(_.userId)
} yield Ok(
@ -269,7 +268,7 @@ final class User(
def topNb(nb: Int, perfKey: String) = Open { implicit ctx =>
PerfType(perfKey) ?? { perfType =>
env.user.cached top200Perf perfType.id map { _ take (nb atLeast 1 atMost 200) } flatMap { users =>
env.user.cached.top200Perf get perfType.id dmap { _ take (nb atLeast 1 atMost 200) } flatMap { users =>
negotiate(
html = Ok(html.user.top(perfType, users)).fuccess,
api = _ =>
@ -286,7 +285,7 @@ final class User(
negotiate(
html = notFound,
api = _ =>
env.user.cached.topWeek(()).map { users =>
env.user.cached.topWeek.map { users =>
Ok(Json toJson users.map(env.user.jsonView.lightPerfIsOnline))
}
)
@ -431,7 +430,7 @@ final class User(
perfStat = oldPerfStat.copy(playStreak = oldPerfStat.playStreak.checkCurrent)
ranks = env.user.cached rankingsOf u.id
distribution <- u.perfs(perfType).established ?? {
env.user.cached.ratingDistribution(perfType) map some
env.user.rankingApi.weeklyRatingDistribution(perfType) dmap some
}
percentile = distribution.map { distrib =>
lila.user.Stat.percentile(distrib, u.perfs(perfType).intRating) match {

View File

@ -46,7 +46,7 @@ final class Preload(
simuls.mon(_.lobby segment "simuls") zip
tv.getBestGame.mon(_.lobby segment "tvBestGame") zip
(ctx.userId ?? timelineApi.userEntries).mon(_.lobby segment "timeline") zip
userCached.topWeek(()).mon(_.lobby segment "userTopWeek") zip
userCached.topWeek.mon(_.lobby segment "userTopWeek") zip
tourWinners.all.dmap(_.top).mon(_.lobby segment "tourWinners") zip
(ctx.noBot ?? dailyPuzzle()).mon(_.lobby segment "puzzle") zip
liveStreamApi.all.dmap(_.autoFeatured withTitles lightUserApi).mon(_.lobby segment "streams") zip

View File

@ -366,7 +366,6 @@ tournament {
sri.timeout = 7 seconds # small to avoid missed events
api_actor.name = tournament-api
pairing.delay = 3.1 seconds
leaderboard.cache.ttl = 1 hour
}
simul {
collection.simul = simul

View File

@ -62,6 +62,16 @@ object mon {
gauge("caffeine.eviction.count").withTag("name", name).update(stats.evictionCount)
gauge("caffeine.entry.count").withTag("name", name).update(cache.estimatedSize)
}
object mongoCache {
def request(name: String, hit: Boolean) =
counter("mongocache.request").withTags(
Map(
"name" -> name,
"hit" -> hit
)
)
def compute(name: String) = timer("mongocache.compute").withTag("name", name)
}
object evalCache {
private val r = counter("evalCache.request")
def request(ply: Int, isHit: Boolean) =

View File

@ -9,35 +9,46 @@ import lila.user.User
final class Cached(
gameRepo: GameRepo,
cacheApi: lila.memo.CacheApi,
mongoCache: MongoCache.Builder
mongoCache: MongoCache.Api
)(implicit ec: scala.concurrent.ExecutionContext) {
def nbImportedBy(userId: User.ID): Fu[Int] = nbImportedCache(userId)
def clearNbImportedByCache = nbImportedCache remove _
def nbImportedBy(userId: User.ID): Fu[Int] = nbImportedCache.get(userId)
def clearNbImportedByCache = nbImportedCache invalidate _
def nbTotal: Fu[Int] = countCache($empty)
def nbTotal: Fu[Int] = nbTotalCache.get({})
def nbPlaying = nbPlayingCache.get _
private val nbPlayingCache = cacheApi[User.ID, Int](256, "game.nbPlaying") {
_.expireAfterAccess(15 seconds)
_.expireAfterWrite(15 seconds)
.buildAsyncFuture { userId =>
gameRepo.coll.countSel(Query nowPlaying userId)
}
}
private val nbImportedCache = mongoCache[User.ID, Int](
prefix = "game:imported",
f = userId => gameRepo.coll countSel Query.imported(userId),
timeToLive = 1 hour,
timeToLiveMongo = 30.days.some,
keyToString = identity
)
1024,
"game:imported",
30 days,
_.toString
) { loader =>
_.expireAfterAccess(10 minutes)
.buildAsyncFuture {
loader { userId =>
gameRepo.coll countSel Query.imported(userId)
}
}
}
private val countCache = mongoCache[Bdoc, Int](
prefix = "game:count",
f = gameRepo.coll.countSel(_),
timeToLive = 1 hour,
keyToString = lila.db.BSON.hashDoc
)
private val nbTotalCache = mongoCache.unit[Int](
"game:total",
29 minutes
) { loader =>
_.refreshAfterWrite(30 minutes)
.buildAsyncFuture {
loader { _ =>
gameRepo.coll.countSel($empty)
}
}
}
}

View File

@ -27,7 +27,7 @@ final class Env(
db: lila.db.Db,
baseUrl: BaseUrl,
userRepo: lila.user.UserRepo,
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
getLightUser: lila.common.LightUser.Getter,
cacheApi: lila.memo.CacheApi
)(implicit ec: scala.concurrent.ExecutionContext, system: ActorSystem, scheduler: Scheduler) {

View File

@ -1,20 +1,17 @@
package lila.history
import com.softwaremill.macwire._
import scala.concurrent.duration._
import lila.common.config.CollName
@Module
final class Env(
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
userRepo: lila.user.UserRepo,
cacheApi: lila.memo.CacheApi,
db: lila.db.Db
)(implicit ec: scala.concurrent.ExecutionContext) {
private val cacheTtl = 30 minutes
private lazy val coll = db(CollName("history3"))
lazy val api = wire[HistoryApi]

View File

@ -9,11 +9,10 @@ import lila.user.User
final class RatingChartApi(
historyApi: HistoryApi,
mongoCache: lila.memo.MongoCache.Builder,
cacheTtl: FiniteDuration
mongoCache: lila.memo.MongoCache.Api
)(implicit ec: scala.concurrent.ExecutionContext) {
def apply(user: User): Fu[Option[String]] = cache(user) dmap { chart =>
def apply(user: User): Fu[Option[String]] = cache.get(user) dmap { chart =>
chart.nonEmpty option chart
}
@ -23,12 +22,19 @@ final class RatingChartApi(
} map JsArray.apply
private val cache = mongoCache[User, String](
prefix = "history:rating",
f = user => build(user) dmap (~_),
maxCapacity = 1024,
timeToLive = cacheTtl,
keyToString = _.id
)
1024,
"history:rating",
60 minutes,
_.id
) { loader =>
_.expireAfterAccess(10 minutes)
.maximumSize(2048)
.buildAsyncFuture {
loader { user =>
build(user) dmap (~_)
}
}
}
private def ratingsMapToJson(user: User, ratingsMap: RatingsMap) = ratingsMap.map {
case (days, rating) =>

View File

@ -7,27 +7,29 @@ import play.api.Mode
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
final class CacheApi(mode: Mode)(implicit ec: ExecutionContext, system: ActorSystem) {
final class CacheApi(
mode: Mode
)(implicit ec: ExecutionContext, system: ActorSystem) {
import CacheApi._
// AsyncLoadingCache with monitoring
def apply[K, V](initialCapacity: Int, name: String)(
build: Builder => AsyncLoadingCache[K, V]
): AsyncLoadingCache[K, V] = {
val actualCapacity =
if (mode != Mode.Prod) math.sqrt(initialCapacity).toInt atLeast 1
else initialCapacity
val cache = build {
scaffeine.recordStats.initialCapacity(actualCapacity)
scaffeine.recordStats.initialCapacity(actualCapacity(initialCapacity))
}
monitor(name, cache)
cache
}
// AsyncLoadingCache for a single entry
def unit[V](build: Builder => AsyncLoadingCache[Unit, V]): AsyncLoadingCache[Unit, V] = {
build(scaffeine initialCapacity 1)
}
// AsyncLoadingCache with monitoring and a synchronous getter
def sync[K, V](
name: String,
initialCapacity: Int,
@ -52,11 +54,15 @@ final class CacheApi(mode: Mode)(implicit ec: ExecutionContext, system: ActorSys
def monitor(name: String, cache: caffeine.cache.Cache[_, _]): Unit =
startMonitor(name, cache)
def actualCapacity(c: Int) =
if (mode != Mode.Prod) math.sqrt(c).toInt atLeast 1
else c
}
object CacheApi {
private type Builder = Scaffeine[Any, Any]
private[memo] type Builder = Scaffeine[Any, Any]
def scaffeine: Builder = Scaffeine().scheduler(caffeine.cache.Scheduler.systemScheduler)

View File

@ -20,11 +20,11 @@ final class Env(
private val config = appConfig.get[MemoConfig]("memo")(AutoConfig.loader)
lazy val mongoCache = wire[MongoCache.Builder]
lazy val configStore = wire[ConfigStore.Builder]
lazy val settingStore = wire[SettingStore.Builder]
lazy val cacheApi = wire[CacheApi]
lazy val mongoCacheApi = wire[MongoCache.Api]
}

View File

@ -1,108 +1,104 @@
package lila.memo
import com.github.blemale.scaffeine.Cache
import com.github.blemale.scaffeine.AsyncLoadingCache
import org.joda.time.DateTime
import reactivemongo.api.bson._
import scala.concurrent.duration._
import CacheApi._
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
/**
* To avoid recomputing very expensive values after deploy
*/
final class MongoCache[K, V: BSONHandler] private (
prefix: String,
cache: Cache[K, Fu[V]],
mongoExpiresAt: () => DateTime,
val coll: Coll,
f: K => Fu[V],
keyToString: K => String
name: String,
dbTtl: FiniteDuration,
keyToString: K => String,
build: MongoCache.LoaderWrapper[K, V] => AsyncLoadingCache[K, V],
val coll: Coll
)(implicit ec: scala.concurrent.ExecutionContext) {
private case class Entry(_id: String, v: V, e: DateTime)
implicit private val entryBSONHandler = Macros.handler[Entry]
def apply(k: K): Fu[V] =
cache.get(
k,
k =>
coll.find($id(makeKey(k)), none[Bdoc]).one[Entry] flatMap {
case None =>
f(k) flatMap { v =>
persist(k, v) inject v
}
case Some(entry) => fuccess(entry.v)
}
)
def remove(k: K): Funit = {
val fut = f(k)
cache.put(k, fut)
fut flatMap { v =>
persist(k, v).void
private val cache = build { loader => k =>
val dbKey = makeDbKey(k)
coll.one[Entry]($id(dbKey)) flatMap {
case None =>
lila.mon.mongoCache.request(name, false)
loader(k)
.flatMap { v =>
coll.update.one(
$id(dbKey),
Entry(dbKey, v, DateTime.now.plusSeconds(dbTtl.toSeconds.toInt)),
upsert = true
) inject v
}
.mon(_.mongoCache.compute(name))
case Some(entry) =>
lila.mon.mongoCache.request(name, false)
fuccess(entry.v)
}
}
private def makeKey(k: K) = s"$prefix:${keyToString(k)}"
def get = cache.get _
private def persist(k: K, v: V): Funit = {
val mongoKey = makeKey(k)
coll.update
.one(
$id(mongoKey),
Entry(mongoKey, v, mongoExpiresAt()),
upsert = true
)
.void
}
def invalidate(key: K): Funit =
coll.delete.one($id(makeDbKey(key))).void >>-
cache.invalidate(key)
private def makeDbKey(key: K) = s"$name:${keyToString(key)}"
}
object MongoCache {
// expire in mongo 3 seconds before in heap,
// to make sure the mongo cache is cleared
// when the heap value expires
private def mongoExpiresAt(ttl: FiniteDuration): () => DateTime = {
val seconds = ttl.toSeconds.toInt - 3
() => DateTime.now plusSeconds seconds
}
type Loader[K, V] = K => Fu[V]
type LoaderWrapper[K, V] = Loader[K, V] => Loader[K, V]
final class Builder(db: lila.db.Db, config: MemoConfig)(implicit ec: scala.concurrent.ExecutionContext) {
final class Api(
db: lila.db.Db,
config: MemoConfig,
cacheApi: CacheApi
)(implicit ec: scala.concurrent.ExecutionContext) {
val coll = db(config.cacheColl)
private val coll = db(config.cacheColl)
// AsyncLoadingCache with monitoring and DB persistence
def apply[K, V: BSONHandler](
prefix: String,
f: K => Fu[V],
maxCapacity: Int = 1024,
timeToLive: FiniteDuration,
timeToLiveMongo: Option[FiniteDuration] = None,
initialCapacity: Int,
name: String,
dbTtl: FiniteDuration,
keyToString: K => String
): MongoCache[K, V] = new MongoCache[K, V](
prefix = prefix,
cache = CacheApi.scaffeine
.expireAfterWrite(timeToLive)
.maximumSize(maxCapacity)
.build[K, Fu[V]],
mongoExpiresAt = mongoExpiresAt(timeToLiveMongo | timeToLive),
coll = coll,
f = f,
keyToString = keyToString
)
)(
build: LoaderWrapper[K, V] => Builder => AsyncLoadingCache[K, V]
): MongoCache[K, V] = {
val cache = new MongoCache(
name,
dbTtl,
keyToString,
(wrapper: LoaderWrapper[K, V]) =>
build(wrapper)(scaffeine.recordStats.initialCapacity(cacheApi.actualCapacity(initialCapacity))),
coll
)
cacheApi.monitor(name, cache.cache)
cache
}
def single[V: BSONHandler](
prefix: String,
f: => Fu[V],
timeToLive: FiniteDuration
) = new MongoCache[Unit, V](
prefix = prefix,
cache = CacheApi.scaffeine
.expireAfterWrite(timeToLive)
.maximumSize(1)
.build[Unit, Fu[V]],
mongoExpiresAt = mongoExpiresAt(timeToLive),
coll = coll,
f = _ => f,
keyToString = _.toString
// AsyncLoadingCache for single entry with DB persistence
def unit[V: BSONHandler](
name: String,
dbTtl: FiniteDuration
)(
build: LoaderWrapper[Unit, V] => Builder => AsyncLoadingCache[Unit, V]
): MongoCache[Unit, V] = new MongoCache(
name,
dbTtl,
_ => "",
wrapper => build(wrapper)(scaffeine.initialCapacity(1)),
coll
)
}
}

View File

@ -13,13 +13,14 @@ import lila.db.dsl._
final private class CheckMail(
ws: WSClient,
config: SecurityConfig.CheckMail,
mongoCache: lila.memo.MongoCache.Builder
mongoCache: lila.memo.MongoCache.Api
)(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem) {
def apply(domain: Domain.Lower): Fu[Boolean] =
if (config.key.value.isEmpty) fuccess(true)
else
cache(domain)
cache
.get(domain)
.withTimeoutDefault(2.seconds, true)
.recover {
case e: Exception =>
@ -42,11 +43,14 @@ final private class CheckMail(
private val prefix = "security:check_mail"
private val cache = mongoCache[Domain.Lower, Boolean](
prefix = prefix,
f = fetch,
timeToLive = 1000 days,
keyToString = _.toString
)
512,
prefix,
1000 days,
_.toString
) { loader =>
_.maximumSize(512)
.buildAsyncFuture(loader(fetch))
}
private def fetch(domain: Domain.Lower): Fu[Boolean] =
ws.url(config.url)

View File

@ -24,7 +24,7 @@ final class Env(
cacheApi: lila.memo.CacheApi,
settingStore: lila.memo.SettingStore.Builder,
tryOAuthServer: OAuthServer.Try,
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
db: lila.db.Db
)(implicit ec: scala.concurrent.ExecutionContext, system: ActorSystem, scheduler: Scheduler) {

View File

@ -16,7 +16,6 @@ private class TournamentConfig(
@ConfigName("collection.player") val playerColl: CollName,
@ConfigName("collection.pairing") val pairingColl: CollName,
@ConfigName("collection.leaderboard") val leaderboardColl: CollName,
@ConfigName("leaderboard.cache.ttl") val leaderboardCacheTtl: FiniteDuration,
@ConfigName("api_actor.name") val apiActorName: String
)
@ -24,7 +23,7 @@ private class TournamentConfig(
final class Env(
appConfig: Configuration,
db: lila.db.Db,
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
cacheApi: lila.memo.CacheApi,
gameRepo: lila.game.GameRepo,
userRepo: lila.user.UserRepo,

View File

@ -9,21 +9,24 @@ import lila.db.dsl._
final class TournamentStatsApi(
playerRepo: PlayerRepo,
pairingRepo: PairingRepo,
mongoCache: lila.memo.MongoCache.Builder
mongoCache: lila.memo.MongoCache.Api
)(implicit ec: scala.concurrent.ExecutionContext) {
def apply(tournament: Tournament): Fu[Option[TournamentStats]] =
tournament.isFinished ?? cache(tournament.id).dmap(some)
tournament.isFinished ?? cache.get(tournament.id).dmap(some)
implicit private val statsBSONHandler = Macros.handler[TournamentStats]
private val cache = mongoCache[String, TournamentStats](
prefix = "tournament:stats",
keyToString = identity,
f = fetch,
timeToLive = 10 minutes,
timeToLiveMongo = 90.days.some
)
private val cache = mongoCache[Tournament.ID, TournamentStats](
32,
"tournament:stats",
30 days,
identity
) { loader =>
_.expireAfterAccess(10 minutes)
.maximumSize(256)
.buildAsyncFuture(loader(fetch))
}
private def fetch(tournamentId: Tournament.ID): Fu[TournamentStats] =
for {

View File

@ -58,8 +58,7 @@ case class AllWinners(
final class WinnersApi(
tournamentRepo: TournamentRepo,
mongoCache: lila.memo.MongoCache.Builder,
ttl: FiniteDuration,
mongoCache: lila.memo.MongoCache.Api,
scheduler: akka.actor.Scheduler
)(implicit ec: scala.concurrent.ExecutionContext) {
@ -124,18 +123,20 @@ final class WinnersApi(
)
}
private val allCache = mongoCache.single[AllWinners](
prefix = "tournament:winner:all",
f = fetchAll,
timeToLive = ttl
)
private val allCache = mongoCache.unit[AllWinners](
"tournament:winner:all",
59 minutes
) { loader =>
_.refreshAfterWrite(1 hour)
.buildAsyncFuture(loader(_ => fetchAll))
}
def all: Fu[AllWinners] = allCache(())
def all: Fu[AllWinners] = allCache.get({})
// because we read on secondaries, delay cache clear
def clearCache(tour: Tournament) =
if (tour.schedule.exists(_.freq.isDailyOrBetter))
scheduler.scheduleOnce(5.seconds) { allCache.remove(()) }
scheduler.scheduleOnce(5.seconds) { allCache.invalidate({}) }
}

View File

@ -11,7 +11,7 @@ import User.{ LightCount, LightPerf }
final class Cached(
userRepo: UserRepo,
onlineUserIds: () => Set[User.ID],
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
cacheApi: lila.memo.CacheApi,
rankingApi: RankingApi
)(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem) {
@ -29,31 +29,49 @@ final class Cached(
)
val top200Perf = mongoCache[Perf.ID, List[User.LightPerf]](
prefix = "user:top200:perf",
f = (perf: Perf.ID) => rankingApi.topPerf(perf, 200),
timeToLive = 16 minutes,
keyToString = _.toString
)
private val topWeekCache = mongoCache.single[List[User.LightPerf]](
prefix = "user:top:week",
f = PerfType.leaderboardable
.map { perf =>
rankingApi.topPerf(perf.id, 1)
PerfType.leaderboardable.size,
"user:top200:perf",
19 minutes,
_.toString
) { loader =>
_.refreshAfterWrite(20 minutes)
.buildAsyncFuture {
loader {
rankingApi.topPerf(_, 200)
}
}
.sequenceFu
.map(_.flatten),
timeToLive = 9 minutes
)
}
def topWeek = topWeekCache.apply _
private val topWeekCache = mongoCache.unit[List[User.LightPerf]](
"user:top:week",
9 minutes
) { loader =>
_.refreshAfterWrite(10 minutes)
.buildAsyncFuture {
loader { _ =>
PerfType.leaderboardable
.map { perf =>
rankingApi.topPerf(perf.id, 1)
}
.sequenceFu
.dmap(_.flatten)
}
}
}
val topNbGame = mongoCache[Int, List[User.LightCount]](
prefix = "user:top:nbGame",
f = nb => userRepo topNbGame nb map { _ map (_.lightCount) },
timeToLive = 74 minutes,
keyToString = _.toString
)
def topWeek = topWeekCache.get({})
val top10NbGame = mongoCache.unit[List[User.LightCount]](
"user:top:nbGame",
74 minutes
) { loader =>
_.refreshAfterWrite(75 minutes)
.buildAsyncFuture {
loader { _ =>
userRepo topNbGame 10 dmap (_.map(_.lightCount))
}
}
}
private val top50OnlineCache = new lila.memo.PeriodicRefreshCache[List[User]](
every = Every(30 seconds),
@ -66,10 +84,6 @@ final class Cached(
def rankingsOf(userId: User.ID): Map[PerfType, Int] = rankingApi.weeklyStableRanking of userId
object ratingDistribution {
def apply(perf: PerfType) = rankingApi.weeklyRatingDistribution(perf)
}
private[user] val botIds = cacheApi.unit[Set[User.ID]] {
_.refreshAfterWrite(10 minutes)
.buildAsyncFuture(_ => userRepo.botIds)

View File

@ -25,7 +25,7 @@ private class UserConfig(
final class Env(
appConfig: Configuration,
db: lila.db.Db,
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
cacheApi: lila.memo.CacheApi,
timeline: lila.hub.actors.Timeline,
isOnline: lila.socket.IsOnline,

View File

@ -13,7 +13,7 @@ import lila.rating.{ Glicko, Perf, PerfType }
final class RankingApi(
userRepo: UserRepo,
coll: Coll,
mongoCache: lila.memo.MongoCache.Builder,
mongoCache: lila.memo.MongoCache.Api,
lightUser: lila.common.LightUser.Getter
)(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem) {
@ -160,14 +160,19 @@ final class RankingApi(
private type NbUsers = Int
def apply(perf: PerfType) = cache(perf.id)
def apply(pt: PerfType) = cache.get(pt.id)
private val cache = mongoCache[Perf.ID, List[NbUsers]](
prefix = "user:rating:distribution",
f = compute,
timeToLive = 3 hour,
keyToString = _.toString
)
PerfType.leaderboardable.size,
"user:rating:distribution",
179 minutes,
_.toString
) { loader =>
_.refreshAfterWrite(180 minutes)
.buildAsyncFuture {
loader(compute)
}
}
// from 600 to 2800 by Stat.group
private def compute(perfId: Perf.ID): Fu[List[NbUsers]] =

View File

@ -11,7 +11,7 @@ final class TrophyApi(
coll: Coll,
kindColl: Coll,
cacheApi: CacheApi
)(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem) {
)(implicit ec: scala.concurrent.ExecutionContext) {
private val trophyKindObjectBSONHandler = Macros.handler[TrophyKind]