use async team caches whenever possible

pull/2571/head
Thibault Duplessis 2017-01-25 11:41:08 +01:00
parent 68d4fe196c
commit 5b9a3a840a
17 changed files with 98 additions and 70 deletions

View File

@ -40,6 +40,7 @@ final class Env(
getRanks = Env.user.cached.ranking.getAll,
isHostingSimul = Env.simul.isHosting,
fetchIsStreamer = Env.tv.isStreamer.apply,
fetchTeamIds = Env.team.api.teamIds,
fetchIsCoach = Env.coach.api.isListedCoach,
insightShare = Env.insight.share,
getPlayTime = Env.game.playTime.apply,
@ -62,7 +63,8 @@ final class Env(
system.actorOf(Props(new actor.Renderer), name = RendererName)
lila.log.boot.info("Preloading modules")
lila.common.Chronometer.syncEffect(List(Env.socket,
lila.common.Chronometer.syncEffect(List(
Env.socket,
Env.site,
Env.tournament,
Env.lobby,

View File

@ -7,7 +7,10 @@ object ForumCateg extends LilaController with ForumController {
def index = Open { implicit ctx =>
NotForKids {
categApi.list(ctx.userId ?? teamCache.teamIds, ctx.troll) map { html.forum.categ.index(_) }
for {
teamIds <- ctx.userId ?? teamCache.teamIds
categs <- categApi.list(teamIds, ctx.troll)
} yield html.forum.categ.index(categs)
}
}
@ -15,8 +18,11 @@ object ForumCateg extends LilaController with ForumController {
NotForKids {
Reasonable(page, 50, errorPage = notFound) {
CategGrantRead(slug) {
OptionOk(categApi.show(slug, page, ctx.troll)) {
case (categ, topics) => html.forum.categ.show(categ, topics)
OptionFuOk(categApi.show(slug, page, ctx.troll)) {
case (categ, topics) =>
isGrantedWrite(categ.slug) map { canWrite =>
html.forum.categ.show(categ, topics, canWrite)
}
}
}
}

View File

@ -15,20 +15,23 @@ private[controllers] trait ForumController extends forum.Granter { self: LilaCon
protected def teamCache = Env.team.cached
protected def userBelongsToTeam(teamId: String, userId: String): Boolean =
protected def userBelongsToTeam(teamId: String, userId: String): Fu[Boolean] =
Env.team.api.belongsTo(teamId, userId)
protected def userOwnsTeam(teamId: String, userId: String): Fu[Boolean] =
Env.team.api.owns(teamId, userId)
protected def CategGrantRead[A <: Result](categSlug: String)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
isGrantedRead(categSlug).fold(a,
isGrantedRead(categSlug).fold(
a,
fuccess(Forbidden("You cannot access to this category"))
)
protected def CategGrantWrite[A <: Result](categSlug: String)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
if (isGrantedWrite(categSlug)) a
else fuccess(Forbidden("You cannot post to this category"))
isGrantedWrite(categSlug) flatMap { granted =>
if (granted) a
else fuccess(Forbidden("You cannot post to this category"))
}
protected def CategGrantMod[A <: Result](categSlug: String)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
isGrantedMod(categSlug) flatMap { granted =>

View File

@ -48,7 +48,8 @@ object ForumTopic extends LilaController with ForumController {
OptionFuOk(topicApi.show(categSlug, slug, page, ctx.troll)) {
case (categ, topic, posts) => for {
unsub <- ctx.userId ?? Env.timeline.status(s"forum:${topic.id}")
form <- (!posts.hasNextPage && isGrantedWrite(categSlug) && topic.open) ?? forms.postWithCaptcha.map(_.some)
canWrite <- isGrantedWrite(categSlug)
form <- (!posts.hasNextPage && canWrite && topic.open) ?? forms.postWithCaptcha.map(_.some)
canModCateg <- isGrantedMod(categ.slug)
} yield html.forum.topic.show(categ, topic, posts, form, unsub, canModCateg = canModCateg)
}

View File

@ -24,8 +24,10 @@ object Team extends LilaController {
def home(page: Int) = Open { implicit ctx =>
NotForKids {
if (ctx.me.??(api.hasTeams)) Redirect(routes.Team.mine).fuccess
else Redirect(routes.Team.all(page)).fuccess
ctx.me.??(api.hasTeams) map {
case true => Redirect(routes.Team.mine)
case false => Redirect(routes.Team.all(page))
}
}
}

View File

@ -40,7 +40,7 @@ final class TeamInfoApi(
def apply(team: Team, me: Option[User]): Fu[TeamInfo] = for {
requests (team.enabled && me.??(m => team.isCreator(m.id))) ?? api.requestsWithUsers(team)
mine = me.??(m => api.belongsTo(team.id, m.id))
mine <- me.??(m => api.belongsTo(team.id, m.id))
requestedByMe !mine ?? me.??(m => RequestRepo.exists(team.id, m.id))
cachable <- cache(team.id)
forumNbPosts getForumNbPosts(team.id)

View File

@ -24,6 +24,7 @@ case class UserInfo(
nbStudies: Int,
playTime: User.PlayTime,
trophies: Trophies,
teamIds: Set[String],
isStreamer: Boolean,
isCoach: Boolean,
insightVisible: Boolean,
@ -73,6 +74,7 @@ object UserInfo {
getRanks: String => Fu[Map[String, Int]],
isHostingSimul: String => Fu[Boolean],
fetchIsStreamer: String => Fu[Boolean],
fetchTeamIds: User.ID => Fu[Set[String]],
fetchIsCoach: User => Fu[Boolean],
insightShare: lila.insight.Share,
getPlayTime: User => Fu[User.PlayTime],
@ -87,13 +89,14 @@ object UserInfo {
postApi.nbByUser(user.id) zip
studyRepo.countByOwner(user.id) zip
trophyApi.findByUser(user) zip
fetchTeamIds(user.id) zip
fetchIsCoach(user) zip
fetchIsStreamer(user.id) zip
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
getPlayTime(user) zip
completionRate(user.id) zip
bookmarkApi.countByUser(user) flatMap {
case (((((((((((((((ranks, nbPlaying), nbImported), crosstable), ratingChart), nbFollowers), nbBlockers), nbPosts), nbStudies), trophies), isCoach), isStreamer), insightVisible), playTime), completionRate), nbBookmarks) =>
case ((((((((((((((((ranks, nbPlaying), nbImported), crosstable), ratingChart), nbFollowers), nbBlockers), nbPosts), nbStudies), trophies), teamIds), isCoach), isStreamer), insightVisible), playTime), completionRate), nbBookmarks) =>
(nbPlaying > 0) ?? isHostingSimul(user.id) map { hasSimul =>
new UserInfo(
user = user,
@ -110,6 +113,7 @@ object UserInfo {
nbStudies = nbStudies,
playTime = playTime,
trophies = trophies,
teamIds = teamIds,
isStreamer = isStreamer,
isCoach = isCoach,
insightVisible = insightVisible,

View File

@ -10,7 +10,7 @@ trait ForumHelper { self: UserHelper with StringHelper =>
private object Granter extends lila.forum.Granter {
protected def userBelongsToTeam(teamId: String, userId: String): Boolean =
protected def userBelongsToTeam(teamId: String, userId: String): Fu[Boolean] =
Env.team.api.belongsTo(teamId, userId)
protected def userOwnsTeam(teamId: String, userId: String): Fu[Boolean] =

View File

@ -12,9 +12,7 @@ trait TeamHelper {
private def api = teamEnv.api
def myTeam(teamId: String)(implicit ctx: Context): Boolean =
ctx.me.??(me => api.belongsTo(teamId, me.id))
def teamIds(userId: String): Set[String] = api teamIds userId
ctx.me.??(me => api.syncBelongsTo(teamId, me.id))
def teamIdToName(id: String): String = api teamName id getOrElse id

View File

@ -1,7 +1,7 @@
@(categ: lila.forum.Categ, topics: Paginator[lila.forum.TopicView])(implicit ctx: Context)
@(categ: lila.forum.Categ, topics: Paginator[lila.forum.TopicView], canWrite: Boolean)(implicit ctx: Context)
@newTopicButton = {
@if(isGrantedWrite(categ.slug)) {
@if(canWrite) {
<a href="@routes.ForumTopic.form(categ.slug)" class="action button" data-icon="m"> @trans.createANewTopic()</a>
}
}

View File

@ -209,8 +209,8 @@ description = describeUser(u)).some) {
<p>@trans.tpTimeSpentOnTV(showPeriod(tvPeriod))</p>
}
<div class="teams">
@teamIds(u.id).toList.sortBy(t => !myTeam(t)).map { teamId =>
@teamLink(teamId, myTeam(teamId).option("mine"))
@info.teamIds.toList.map(t => t -> myTeam(t)).sortBy(t => (!t._2, t._1)).map { t =>
@teamLink(t._1, t._2.option("mine"))
}
</div>
</div>

View File

@ -1,7 +1,7 @@
package lila.forum
import lila.security.{ Permission, Granter => Master }
import lila.user.UserContext
import lila.user.{ User, UserContext }
import org.joda.time.DateTime
trait Granter {
@ -9,7 +9,7 @@ trait Granter {
private val TeamSlugPattern = """^team-([\w-]+)$""".r
private val StaffSlug = "staff"
protected def userBelongsToTeam(teamId: String, userId: String): Boolean
protected def userBelongsToTeam(teamId: String, userId: String): Fu[Boolean]
protected def userOwnsTeam(teamId: String, userId: String): Fu[Boolean]
def isGrantedRead(categSlug: String)(implicit ctx: UserContext): Boolean =
@ -17,21 +17,18 @@ trait Granter {
ctx.me exists Master(Permission.StaffForum),
true)
def isGrantedWrite(categSlug: String)(implicit ctx: UserContext): Boolean =
isOldEnoughToForum && {
ctx.me ?? { me =>
Master(Permission.StaffForum)(me) || {
categSlug match {
case StaffSlug => false
case TeamSlugPattern(teamId) => userBelongsToTeam(teamId, me.id)
case _ => true
}
}
def isGrantedWrite(categSlug: String)(implicit ctx: UserContext): Fu[Boolean] =
ctx.me.filter(isOldEnoughToForum) ?? { me =>
if (Master(Permission.StaffForum)(me)) fuccess(true)
else categSlug match {
case StaffSlug => fuccess(false)
case TeamSlugPattern(teamId) => userBelongsToTeam(teamId, me.id)
case _ => fuccess(true)
}
}
def isOldEnoughToForum(implicit ctx: UserContext) =
ctx.me ?? { u => u.count.game > 0 && (u.createdAt isBefore DateTime.now.minusDays(2)) }
private def isOldEnoughToForum(u: User) =
u.count.game > 0 && (u.createdAt isBefore DateTime.now.minusDays(2))
def isGrantedMod(categSlug: String)(implicit ctx: UserContext): Fu[Boolean] =
categSlug match {

View File

@ -13,10 +13,10 @@ private[forum] final class Recent(
nb: Int,
publicCategIds: List[String]) {
private type GetTeams = String => Set[String]
private type GetTeamIds = String => Fu[Set[String]]
def apply(user: Option[User], getTeams: GetTeams): Fu[List[MiniForumPost]] =
userCacheKey(user, getTeams) |> { key => cache(key)(fetch(key)) }
def apply(user: Option[User], getTeams: GetTeamIds): Fu[List[MiniForumPost]] =
userCacheKey(user, getTeams) flatMap { key => cache(key)(fetch(key)) }
def team(teamId: String): Fu[List[MiniForumPost]] = {
// prepend empty language list
@ -26,12 +26,14 @@ private[forum] final class Recent(
def invalidate: Funit = fuccess(cache.clear)
private def userCacheKey(user: Option[User], getTeams: GetTeams): String =
user.fold("en")(_.langs.mkString(",")) :: {
(user.??(_.troll) ?? List("[troll]")) :::
(user ?? MasterGranter(Permission.StaffForum)).fold(staffCategIds, publicCategIds) :::
((user.map(_.id) ?? getTeams) map teamSlug).toList
} mkString ";"
private def userCacheKey(user: Option[User], getTeams: GetTeamIds): Fu[String] =
(user.map(_.id) ?? getTeams).map { teamIds =>
user.fold("en")(_.langs.mkString(",")) :: {
(user.??(_.troll) ?? List("[troll]")) :::
(user ?? MasterGranter(Permission.StaffForum)).fold(staffCategIds, publicCategIds) :::
(teamIds map teamSlug).toList
} mkString ";"
}
private lazy val staffCategIds = "staff" :: publicCategIds

View File

@ -2,7 +2,7 @@ package lila
package object forum extends PackageObject with WithPlay {
private[forum] def teamSlug(id: String) = "team-" + id
private[forum] def teamSlug(id: String) = s"team-$id"
private[forum] val logger = lila.log("forum")
}

View File

@ -15,7 +15,7 @@ private[team] final class Cached(implicit system: akka.actor.ActorSystem) {
def name(id: String) = nameCache sync id
private[team] val teamIdsCache = new Syncache[String, Set[String]](
private val teamIdsCache = new Syncache[String, Set[String]](
name = "team.ids",
compute = MemberRepo.teamIdsByUser,
default = _ => Set.empty,
@ -23,7 +23,10 @@ private[team] final class Cached(implicit system: akka.actor.ActorSystem) {
timeToLive = 2 hours,
logger = logger)
def teamIds(userId: String) = teamIdsCache sync userId
def syncTeamIds = teamIdsCache sync _
def teamIds = teamIdsCache async _
def invalidateTeamIds = teamIdsCache invalidate _
val nbRequests = AsyncCache[String, Int](
name = "team.nbRequests",

View File

@ -35,7 +35,7 @@ final class TeamApi(
createdBy = me)
coll.team.insert(team) >>
MemberRepo.add(team.id, me.id) >>- {
(cached.teamIdsCache invalidate me.id)
(cached invalidateTeamIds me.id)
(forum ! MakeTeam(team.id, team.name))
(indexer ! InsertTeam(team))
(timeline ! Propagate(
@ -49,13 +49,14 @@ final class TeamApi(
location = e.location,
description = e.description,
open = e.isOpen) |> { team =>
coll.team.update($id(team.id), team).void >>- (indexer ! InsertTeam(team))
}
coll.team.update($id(team.id), team).void >>- (indexer ! InsertTeam(team))
}
}
def mine(me: User): Fu[List[Team]] = coll.team.byIds[Team](cached teamIds me.id)
def mine(me: User): Fu[List[Team]] =
cached teamIds me.id flatMap coll.team.byIds[Team]
def hasTeams(me: User): Boolean = cached.teamIds(me.id).nonEmpty
def hasTeams(me: User): Fu[Boolean] = cached.teamIds(me.id).map(_.nonEmpty)
def hasCreatedRecently(me: User): Fu[Boolean] =
TeamRepo.userHasCreatedSince(me.id, creationPeriod)
@ -90,11 +91,10 @@ final class TeamApi(
able teamOption.??(requestable(_, user))
} yield teamOption filter (_ => able)
def requestable(team: Team, user: User): Fu[Boolean] =
RequestRepo.exists(team.id, user.id).map { _ -> belongsTo(team.id, user.id) } map {
case (false, false) => true
case _ => false
}
def requestable(team: Team, user: User): Fu[Boolean] = for {
belongs <- belongsTo(team.id, user.id)
requested <- RequestRepo.exists(team.id, user.id)
} yield !belongs && !requested
def createRequest(team: Team, setup: RequestSetup, user: User): Funit =
requestable(team, user) flatMap {
@ -113,13 +113,15 @@ final class TeamApi(
)
} yield ()
def doJoin(team: Team, userId: String): Funit = (!belongsTo(team.id, userId)) ?? {
MemberRepo.add(team.id, userId) >>
TeamRepo.incMembers(team.id, +1) >>- {
cached.teamIdsCache invalidate userId
timeline ! Propagate(TeamJoin(userId, team.id)).toFollowersOf(userId)
}
} recover lila.db.recoverDuplicateKey(e => ())
def doJoin(team: Team, userId: String): Funit = !belongsTo(team.id, userId) flatMap {
_ ?? {
MemberRepo.add(team.id, userId) >>
TeamRepo.incMembers(team.id, +1) >>- {
cached invalidateTeamIds userId
timeline ! Propagate(TeamJoin(userId, team.id)).toFollowersOf(userId)
}
} recover lila.db.recoverDuplicateKey(e => ())
}
def quit(teamId: String)(implicit ctx: UserContext): Fu[Option[Team]] = for {
teamOption coll.team.byId[Team](teamId)
@ -128,10 +130,12 @@ final class TeamApi(
})
} yield result
def doQuit(team: Team, userId: String): Funit = belongsTo(team.id, userId) ?? {
MemberRepo.remove(team.id, userId) >>
TeamRepo.incMembers(team.id, -1) >>-
(cached.teamIdsCache invalidate userId)
def doQuit(team: Team, userId: String): Funit = belongsTo(team.id, userId) flatMap {
_ ?? {
MemberRepo.remove(team.id, userId) >>
TeamRepo.incMembers(team.id, -1) >>-
(cached invalidateTeamIds userId)
}
}
def quitAll(userId: String): Funit = MemberRepo.removeByUser(userId)
@ -150,8 +154,11 @@ final class TeamApi(
MemberRepo.removeByteam(team.id) >>-
(indexer ! RemoveTeam(team.id))
def belongsTo(teamId: String, userId: String): Boolean =
cached.teamIds(userId) contains teamId
def syncBelongsTo(teamId: String, userId: String): Boolean =
cached.syncTeamIds(userId) contains teamId
def belongsTo(teamId: String, userId: String): Fu[Boolean] =
cached.teamIds(userId) map (_ contains teamId)
def owns(teamId: String, userId: String): Fu[Boolean] =
TeamRepo ownerOf teamId map (Some(userId) ==)

View File

@ -293,6 +293,9 @@ div.user_show .teams a {
display: block;
margin: 5px 0;
}
div.user_show .teams a.mine {
color: #d59120;
}
div.user_show div.games {
margin-top: 10px;
}