621 lines
22 KiB
Scala
621 lines
22 KiB
Scala
package lila.tournament
|
|
|
|
import akka.actor.{ ActorSystem, Props }
|
|
import akka.pattern.ask
|
|
import akka.stream.scaladsl._
|
|
import com.github.ghik.silencer.silent
|
|
import org.joda.time.DateTime
|
|
import play.api.libs.json._
|
|
import scala.concurrent.duration._
|
|
import scala.concurrent.Promise
|
|
|
|
import lila.common.config.MaxPerSecond
|
|
import lila.common.{ Bus, Debouncer, LightUser, WorkQueues }
|
|
import lila.game.{ Game, GameRepo, LightPov }
|
|
import lila.hub.actorApi.lobby.ReloadTournaments
|
|
import lila.hub.LightTeam
|
|
import lila.hub.LightTeam._
|
|
import lila.round.actorApi.round.{ AbortForce, GoBerserk }
|
|
import lila.socket.Socket.SendToFlag
|
|
import lila.user.{ User, UserRepo }
|
|
import makeTimeout.short
|
|
|
|
final class TournamentApi(
|
|
cached: Cached,
|
|
userRepo: UserRepo,
|
|
gameRepo: GameRepo,
|
|
playerRepo: PlayerRepo,
|
|
pairingRepo: PairingRepo,
|
|
tournamentRepo: TournamentRepo,
|
|
apiJsonView: ApiJsonView,
|
|
autoPairing: AutoPairing,
|
|
pairingSystem: arena.PairingSystem,
|
|
callbacks: TournamentApi.Callbacks,
|
|
renderer: lila.hub.actors.Renderer,
|
|
socket: TournamentSocket,
|
|
tellRound: lila.round.TellRound,
|
|
trophyApi: lila.user.TrophyApi,
|
|
verify: Condition.Verify,
|
|
duelStore: DuelStore,
|
|
pause: Pause,
|
|
cacheApi: lila.memo.CacheApi,
|
|
lightUserApi: lila.user.LightUserApi,
|
|
proxyRepo: lila.round.GameProxyRepo
|
|
)(
|
|
implicit ec: scala.concurrent.ExecutionContext,
|
|
system: ActorSystem,
|
|
mat: akka.stream.Materializer,
|
|
mode: play.api.Mode
|
|
) {
|
|
|
|
private val workQueue =
|
|
new WorkQueues(buffer = 256, expiration = 1 minute, timeout = 10 seconds, name = "tournament")
|
|
|
|
def createTournament(
|
|
setup: TournamentSetup,
|
|
me: User,
|
|
myTeams: List[LightTeam],
|
|
getUserTeamIds: User => Fu[List[TeamID]]
|
|
): Fu[Tournament] = {
|
|
val tour = Tournament.make(
|
|
by = Right(me),
|
|
name = DataForm.canPickName(me) ?? setup.name,
|
|
clock = setup.clockConfig,
|
|
minutes = setup.minutes,
|
|
waitMinutes = setup.waitMinutes | DataForm.waitMinuteDefault,
|
|
startDate = setup.startDate,
|
|
mode = setup.realMode,
|
|
password = setup.password,
|
|
variant = setup.realVariant,
|
|
position =
|
|
DataForm.startingPosition(setup.position | chess.StartingPosition.initial.fen, setup.realVariant),
|
|
berserkable = setup.berserkable | true,
|
|
teamBattle = setup.teamBattleByTeam map TeamBattle.init
|
|
) |> { tour =>
|
|
tour.perfType.fold(tour) { perfType =>
|
|
tour.copy(conditions = setup.conditions.convert(perfType, myTeams.view.map(_.pair).toMap))
|
|
}
|
|
}
|
|
if (tour.name != me.titleUsername && lila.common.LameName.anyNameButLichessIsOk(tour.name))
|
|
Bus.publish(lila.hub.actorApi.slack.TournamentName(me.username, tour.id, tour.name), "slack")
|
|
tournamentRepo.insert(tour) >>
|
|
join(tour.id, me, tour.password, setup.teamBattleByTeam, getUserTeamIds, none) inject tour
|
|
}
|
|
|
|
private[tournament] def create(tournament: Tournament): Funit = {
|
|
tournamentRepo.insert(tournament).void
|
|
}
|
|
|
|
def teamBattleUpdate(
|
|
tour: Tournament,
|
|
data: TeamBattle.DataForm.Setup,
|
|
filterExistingTeamIds: Set[TeamID] => Fu[Set[TeamID]]
|
|
): Funit =
|
|
filterExistingTeamIds(data.potentialTeamIds) flatMap { teamIds =>
|
|
tournamentRepo.setTeamBattle(tour.id, TeamBattle(teamIds, data.nbLeaders))
|
|
}
|
|
|
|
private[tournament] def makePairings(forTour: Tournament, users: WaitingUsers): Funit =
|
|
Sequencing(forTour.id)(tournamentRepo.startedById) { tour =>
|
|
cached
|
|
.ranking(tour)
|
|
.mon(_.tournament.pairing.createRanking)
|
|
.flatMap { ranking =>
|
|
pairingSystem
|
|
.createPairings(tour, users, ranking)
|
|
.mon(_.tournament.pairing.createPairings)
|
|
.flatMap {
|
|
case Nil => funit
|
|
case pairings =>
|
|
playerRepo
|
|
.byTourAndUserIds(tour.id, pairings.flatMap(_.users))
|
|
.map {
|
|
_.view.map { player =>
|
|
player.userId -> player
|
|
}.toMap
|
|
}
|
|
.mon(_.tournament.pairing.createPlayerMap)
|
|
.flatMap { playersMap =>
|
|
pairings
|
|
.map { pairing =>
|
|
pairingRepo.insert(pairing) >>
|
|
autoPairing(tour, pairing, playersMap, ranking)
|
|
.mon(_.tournament.pairing.createAutoPairing)
|
|
.map {
|
|
socket.startGame(tour.id, _)
|
|
}
|
|
}
|
|
.sequenceFu
|
|
.mon(_.tournament.pairing.createInserts) >>
|
|
featureOneOf(tour, pairings, ranking)
|
|
.mon(_.tournament.pairing.createFeature) >>-
|
|
lila.mon.tournament.pairing.batchSize.record(pairings.size)
|
|
}
|
|
}
|
|
}
|
|
.monSuccess(_.tournament.pairing.create)
|
|
.chronometer
|
|
.logIfSlow(100, logger)(_ => s"Pairings for https://lichess.org/tournament/${tour.id}")
|
|
.result
|
|
}
|
|
|
|
private def featureOneOf(tour: Tournament, pairings: Pairings, ranking: Ranking): Funit =
|
|
tour.featuredId.ifTrue(pairings.nonEmpty) ?? pairingRepo.byId map2
|
|
RankedPairing(ranking) map (_.flatten) flatMap { curOption =>
|
|
pairings.flatMap(RankedPairing(ranking)).sortBy(_.bestRank).headOption ?? { bestCandidate =>
|
|
def switch = tournamentRepo.setFeaturedGameId(tour.id, bestCandidate.pairing.gameId)
|
|
curOption.filter(_.pairing.playing) match {
|
|
case Some(current) if bestCandidate.bestRank < current.bestRank => switch
|
|
case Some(_) => funit
|
|
case _ => switch
|
|
}
|
|
}
|
|
}
|
|
|
|
def tourAndRanks(game: Game): Fu[Option[TourAndRanks]] = ~ {
|
|
for {
|
|
tourId <- game.tournamentId
|
|
whiteId <- game.whitePlayer.userId
|
|
blackId <- game.blackPlayer.userId
|
|
} yield tournamentRepo byId tourId flatMap {
|
|
_ ?? { tour =>
|
|
cached ranking tour map { ranking =>
|
|
ranking.get(whiteId) |@| ranking.get(blackId) apply {
|
|
case (whiteR, blackR) => TourAndRanks(tour, whiteR + 1, blackR + 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private[tournament] def start(oldTour: Tournament): Funit =
|
|
Sequencing(oldTour.id)(tournamentRepo.createdById) { tour =>
|
|
tournamentRepo.setStatus(tour.id, Status.Started) >>-
|
|
socket.reload(tour.id) >>-
|
|
publish()
|
|
}
|
|
|
|
private[tournament] def destroy(tour: Tournament): Funit =
|
|
tournamentRepo.remove(tour).void >>
|
|
pairingRepo.removeByTour(tour.id) >>
|
|
playerRepo.removeByTour(tour.id) >>- publish() >>- socket.reload(tour.id)
|
|
|
|
private[tournament] def finish(oldTour: Tournament): Funit =
|
|
Sequencing(oldTour.id)(tournamentRepo.startedById) { tour =>
|
|
pairingRepo count tour.id flatMap {
|
|
case 0 => destroy(tour)
|
|
case _ =>
|
|
for {
|
|
_ <- tournamentRepo.setStatus(tour.id, Status.Finished)
|
|
_ <- playerRepo unWithdraw tour.id
|
|
_ <- pairingRepo removePlaying tour.id
|
|
winner <- playerRepo winner tour.id
|
|
_ <- winner.??(p => tournamentRepo.setWinnerId(tour.id, p.userId))
|
|
} yield {
|
|
callbacks.clearJsonViewCache(tour)
|
|
socket.finish(tour.id)
|
|
publish()
|
|
playerRepo withPoints tour.id foreach {
|
|
_ foreach { p =>
|
|
userRepo.incToints(p.userId, p.score)
|
|
}
|
|
}
|
|
awardTrophies(tour).logFailure(logger, _ => s"${tour.id} awardTrophies")
|
|
callbacks.indexLeaderboard(tour).logFailure(logger, _ => s"${tour.id} indexLeaderboard")
|
|
callbacks.clearWinnersCache(tour)
|
|
callbacks.clearTrophyCache(tour)
|
|
duelStore.remove(tour)
|
|
}
|
|
}
|
|
}
|
|
|
|
def kill(tour: Tournament): Funit =
|
|
if (tour.isStarted) finish(tour)
|
|
else if (tour.isCreated) destroy(tour)
|
|
else funit
|
|
|
|
private def awardTrophies(tour: Tournament): Funit = {
|
|
import lila.user.TrophyKind._
|
|
tour.schedule.??(_.freq == Schedule.Freq.Marathon) ?? {
|
|
playerRepo.bestByTourWithRank(tour.id, 100).flatMap {
|
|
_.map {
|
|
case rp if rp.rank == 1 => trophyApi.award(rp.player.userId, marathonWinner)
|
|
case rp if rp.rank <= 10 => trophyApi.award(rp.player.userId, marathonTopTen)
|
|
case rp if rp.rank <= 50 => trophyApi.award(rp.player.userId, marathonTopFifty)
|
|
case rp => trophyApi.award(rp.player.userId, marathonTopHundred)
|
|
}.sequenceFu.void
|
|
}
|
|
}
|
|
}
|
|
|
|
def verdicts(
|
|
tour: Tournament,
|
|
me: Option[User],
|
|
getUserTeamIds: User => Fu[List[TeamID]]
|
|
): Fu[Condition.All.WithVerdicts] = me match {
|
|
case None => fuccess(tour.conditions.accepted)
|
|
case Some(user) => verify(tour.conditions, user, getUserTeamIds)
|
|
}
|
|
|
|
private[tournament] def join(
|
|
tourId: Tournament.ID,
|
|
me: User,
|
|
password: Option[String],
|
|
withTeamId: Option[String],
|
|
getUserTeamIds: User => Fu[List[TeamID]],
|
|
promise: Option[Promise[Boolean]]
|
|
): Funit = Sequencing(tourId)(tournamentRepo.enterableById) { tour =>
|
|
val fuJoined =
|
|
if (tour.password == password) {
|
|
verdicts(tour, me.some, getUserTeamIds) flatMap {
|
|
_.accepted ?? {
|
|
pause.canJoin(me.id, tour) ?? {
|
|
def proceedWithTeam(team: Option[String]) =
|
|
tournamentRepo.tourIdsToWithdrawWhenEntering(tourId).flatMap(withdrawMany(_, me.id)) >>
|
|
playerRepo.join(tour.id, me, tour.perfLens, team) >>
|
|
updateNbPlayers(tour.id) >>- {
|
|
socket.reload(tour.id)
|
|
publish()
|
|
} inject true
|
|
withTeamId match {
|
|
case None if !tour.isTeamBattle => proceedWithTeam(none)
|
|
case None if tour.isTeamBattle =>
|
|
playerRepo.exists(tour.id, me.id) flatMap {
|
|
case true => proceedWithTeam(none)
|
|
case false => fuccess(false)
|
|
}
|
|
case Some(team) =>
|
|
tour.teamBattle match {
|
|
case Some(battle) if battle.teams contains team =>
|
|
getUserTeamIds(me) flatMap { myTeams =>
|
|
if (myTeams has team) proceedWithTeam(team.some)
|
|
// else proceedWithTeam(team.some) // listress
|
|
else fuccess(false)
|
|
}
|
|
case _ => fuccess(false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
socket.reload(tour.id)
|
|
fuccess(false)
|
|
}
|
|
fuJoined map { joined =>
|
|
promise.foreach(_ success joined)
|
|
}
|
|
}
|
|
|
|
def joinWithResult(
|
|
tourId: Tournament.ID,
|
|
me: User,
|
|
password: Option[String],
|
|
teamId: Option[String],
|
|
getUserTeamIds: User => Fu[List[TeamID]]
|
|
): Fu[Boolean] = {
|
|
val promise = Promise[Boolean]
|
|
join(tourId, me, password, teamId, getUserTeamIds, promise.some)
|
|
promise.future.withTimeoutDefault(5.seconds, false)
|
|
}
|
|
|
|
def pageOf(tour: Tournament, userId: User.ID): Fu[Option[Int]] =
|
|
cached ranking tour map {
|
|
_ get userId map { rank =>
|
|
(Math.floor(rank / 10) + 1).toInt
|
|
}
|
|
}
|
|
|
|
private def updateNbPlayers(tourId: Tournament.ID): Funit =
|
|
playerRepo count tourId flatMap { tournamentRepo.setNbPlayers(tourId, _) }
|
|
|
|
def selfPause(tourId: Tournament.ID, userId: User.ID): Funit =
|
|
withdraw(tourId, userId, isPause = true, isStalling = false)
|
|
|
|
private def stallPause(tourId: Tournament.ID, userId: User.ID): Funit =
|
|
withdraw(tourId, userId, isPause = false, isStalling = true)
|
|
|
|
private def withdraw(tourId: Tournament.ID, userId: User.ID, isPause: Boolean, isStalling: Boolean): Funit =
|
|
Sequencing(tourId)(tournamentRepo.enterableById) {
|
|
case tour if tour.isCreated =>
|
|
playerRepo.remove(tour.id, userId) >> updateNbPlayers(tour.id) >>- socket.reload(tour.id) >>- publish()
|
|
case tour if tour.isStarted =>
|
|
for {
|
|
_ <- playerRepo.withdraw(tour.id, userId)
|
|
pausable <- if (isPause) cached.ranking(tour).map { _ get userId exists (7 >) } else
|
|
fuccess(isStalling)
|
|
} yield {
|
|
if (pausable) pause.add(userId)
|
|
socket.reload(tour.id)
|
|
publish()
|
|
}
|
|
case _ => funit
|
|
}
|
|
|
|
private def withdrawMany(tourIds: List[Tournament.ID], userId: User.ID): Funit =
|
|
playerRepo.filterExists(tourIds, userId) flatMap {
|
|
_.map {
|
|
withdraw(_, userId, isPause = false, isStalling = false)
|
|
}.sequenceFu.void
|
|
}
|
|
|
|
def withdrawAll(user: User): Funit =
|
|
tournamentRepo.nonEmptyEnterableIds flatMap { withdrawMany(_, user.id) }
|
|
|
|
private[tournament] def berserk(gameId: Game.ID, userId: User.ID): Funit =
|
|
proxyRepo game gameId flatMap {
|
|
_.filter(_.berserkable) ?? { game =>
|
|
game.tournamentId ?? { tourId =>
|
|
Sequencing(tourId)(tournamentRepo.startedById) { tour =>
|
|
pairingRepo.findPlaying(tour.id, userId) flatMap {
|
|
case Some(pairing) if !pairing.berserkOf(userId) =>
|
|
(pairing colorOf userId) ?? { color =>
|
|
pairingRepo.setBerserk(pairing, userId) >>-
|
|
tellRound(gameId, GoBerserk(color))
|
|
}
|
|
case _ => funit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private[tournament] def finishGame(game: Game): Funit = game.tournamentId ?? { tourId =>
|
|
Sequencing(tourId)(tournamentRepo.startedById) { tour =>
|
|
pairingRepo.finish(game) >>
|
|
game.userIds.map(updatePlayer(tour, game.some)).sequenceFu.void >>- {
|
|
duelStore.remove(game)
|
|
socket.reload(tour.id)
|
|
updateTournamentStanding(tour.id)
|
|
withdrawNonMover(game)
|
|
}
|
|
}
|
|
}
|
|
|
|
private[tournament] def sittingDetected(game: Game, player: User.ID): Funit =
|
|
game.tournamentId ?? { stallPause(_, player) }
|
|
|
|
private def updatePlayer(
|
|
tour: Tournament,
|
|
finishing: Option[Game] // if set, update the player performance. Leave to none to just recompute the sheet.
|
|
)(userId: User.ID): Funit =
|
|
(tour.perfType.ifTrue(tour.mode.rated) ?? { userRepo.perfOf(userId, _) }) flatMap { perf =>
|
|
playerRepo.update(tour.id, userId) { player =>
|
|
cached.sheet.update(tour, userId) map { sheet =>
|
|
player.copy(
|
|
score = sheet.total,
|
|
fire = sheet.onFire,
|
|
rating = perf.fold(player.rating)(_.intRating),
|
|
provisional = perf.fold(player.provisional)(_.provisional),
|
|
performance = {
|
|
for {
|
|
g <- finishing
|
|
performance <- performanceOf(g, userId).map(_.toDouble)
|
|
nbGames = sheet.scores.size
|
|
if nbGames > 0
|
|
} yield Math.round {
|
|
player.performance * (nbGames - 1) / nbGames + performance / nbGames
|
|
} toInt
|
|
} | player.performance
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private def performanceOf(g: Game, userId: String): Option[Int] =
|
|
for {
|
|
opponent <- g.opponentByUserId(userId)
|
|
opponentRating <- opponent.rating
|
|
multiplier = g.winnerUserId.??(winner => if (winner == userId) 1 else -1)
|
|
} yield opponentRating + 500 * multiplier
|
|
|
|
@silent private def withdrawNonMover(game: Game): Unit =
|
|
for {
|
|
tourId <- game.tournamentId
|
|
if game.status == chess.Status.NoStart
|
|
player <- game.playerWhoDidNotMove
|
|
userId <- player.userId
|
|
} withdraw(tourId, userId, isPause = false, isStalling = false)
|
|
|
|
def pausePlaybanned(userId: User.ID) =
|
|
tournamentRepo.startedIds flatMap {
|
|
playerRepo.filterExists(_, userId) flatMap {
|
|
_.map { tourId =>
|
|
playerRepo.withdraw(tourId, userId) >>- socket.reload(tourId) >>- publish()
|
|
}.sequenceFu.void
|
|
}
|
|
}
|
|
|
|
def ejectLame(userId: User.ID, playedIds: List[Tournament.ID]): Funit =
|
|
tournamentRepo.nonEmptyEnterableIds flatMap {
|
|
playerRepo.filterExists(_, userId) flatMap { enteredIds =>
|
|
(enteredIds ++ playedIds)
|
|
.map { ejectLame(_, userId) }
|
|
.sequenceFu
|
|
.void
|
|
}
|
|
}
|
|
|
|
// withdraws the player and forfeits all pairings in ongoing tournaments
|
|
private def ejectLame(tourId: Tournament.ID, userId: User.ID): Funit =
|
|
Sequencing(tourId)(tournamentRepo.byId) { tour =>
|
|
playerRepo.withdraw(tourId, userId) >> {
|
|
if (tour.isStarted)
|
|
pairingRepo.findPlaying(tour.id, userId).map {
|
|
_ foreach { currentPairing =>
|
|
tellRound(currentPairing.gameId, AbortForce)
|
|
}
|
|
} >> pairingRepo.opponentsOf(tour.id, userId).flatMap { uids =>
|
|
pairingRepo.forfeitByTourAndUserId(tour.id, userId) >>
|
|
lila.common.Future.applySequentially(uids.toList)(updatePlayer(tour, none))
|
|
} else if (tour.isFinished && tour.winnerId.contains(userId))
|
|
playerRepo winner tour.id flatMap {
|
|
_ ?? { p =>
|
|
tournamentRepo.setWinnerId(tour.id, p.userId)
|
|
}
|
|
} else funit
|
|
} >>
|
|
updateNbPlayers(tour.id) >>-
|
|
socket.reload(tour.id) >>- publish()
|
|
}
|
|
|
|
private val tournamentTopCache = cacheApi[Tournament.ID, TournamentTop](16, "tournament.top") {
|
|
_.refreshAfterWrite(3 second)
|
|
.expireAfterAccess(5 minutes)
|
|
.maximumSize(64)
|
|
.buildAsyncFuture { id =>
|
|
playerRepo.bestByTour(id, 20) dmap TournamentTop.apply
|
|
}
|
|
}
|
|
|
|
def tournamentTop(tourId: Tournament.ID): Fu[TournamentTop] =
|
|
tournamentTopCache get tourId
|
|
|
|
def miniView(game: Game, withTop: Boolean): Fu[Option[TourMiniView]] =
|
|
withTeamVs(game) flatMap {
|
|
_ ?? {
|
|
case TourAndTeamVs(tour, teamVs) =>
|
|
withTop ?? { tournamentTop(tour.id) map some } map {
|
|
TourMiniView(tour, _, teamVs).some
|
|
}
|
|
}
|
|
}
|
|
|
|
def withTeamVs(game: Game): Fu[Option[TourAndTeamVs]] =
|
|
game.tournamentId ?? tournamentRepo.byId flatMap {
|
|
_ ?? { tour =>
|
|
(tour.isTeamBattle ?? playerRepo.teamVs(tour.id, game)) map {
|
|
TourAndTeamVs(tour, _).some
|
|
}
|
|
}
|
|
}
|
|
|
|
def fetchVisibleTournaments: Fu[VisibleTournaments] =
|
|
tournamentRepo.publicCreatedSorted(6 * 60) zip
|
|
tournamentRepo.publicStarted zip
|
|
tournamentRepo.finishedNotable(30) map {
|
|
case ((created, started), finished) =>
|
|
VisibleTournaments(created, started, finished)
|
|
}
|
|
|
|
def playerInfo(tour: Tournament, userId: User.ID): Fu[Option[PlayerInfoExt]] =
|
|
userRepo named userId flatMap {
|
|
_ ?? { user =>
|
|
playerRepo.find(tour.id, user.id) flatMap {
|
|
_ ?? { player =>
|
|
playerPovs(tour, user.id, 50) map { povs =>
|
|
PlayerInfoExt(user, player, povs).some
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def allCurrentLeadersInStandard: Fu[Map[Tournament, TournamentTop]] =
|
|
tournamentRepo.standardPublicStartedFromSecondary.flatMap { tours =>
|
|
tours
|
|
.map { tour =>
|
|
tournamentTop(tour.id) map (tour -> _)
|
|
}
|
|
.sequenceFu
|
|
.map(_.toMap)
|
|
}
|
|
|
|
def calendar: Fu[List[Tournament]] = {
|
|
val from = DateTime.now.minusDays(1)
|
|
tournamentRepo.calendar(from = from, to = from plusYears 1)
|
|
}
|
|
|
|
def resultStream(tour: Tournament, perSecond: MaxPerSecond, nb: Int): Source[Player.Result, _] =
|
|
playerRepo
|
|
.sortedCursor(tour.id, perSecond.value)
|
|
.documentSource(nb)
|
|
.throttle(perSecond.value, 1 second)
|
|
.zipWithIndex
|
|
.mapAsync(8) {
|
|
case (player, index) =>
|
|
lightUserApi.async(player.userId) map { lu =>
|
|
Player.Result(player, lu | LightUser.fallback(player.userId), index.toInt + 1)
|
|
}
|
|
}
|
|
|
|
def byOwnerStream(owner: User, perSecond: MaxPerSecond, nb: Int): Source[Tournament, _] =
|
|
tournamentRepo
|
|
.sortedCursor(owner, perSecond.value)
|
|
.documentSource(nb)
|
|
.throttle(perSecond.value, 1 second)
|
|
|
|
private def playerPovs(tour: Tournament, userId: User.ID, nb: Int): Fu[List[LightPov]] =
|
|
pairingRepo.recentIdsByTourAndUserId(tour.id, userId, nb) flatMap
|
|
gameRepo.light.gamesFromPrimary map {
|
|
_ flatMap { LightPov.ofUserId(_, userId) }
|
|
}
|
|
|
|
private def Sequencing(
|
|
tourId: Tournament.ID
|
|
)(fetch: Tournament.ID => Fu[Option[Tournament]])(run: Tournament => Funit): Funit =
|
|
workQueue(tourId) {
|
|
fetch(tourId) flatMap {
|
|
_ ?? run
|
|
}
|
|
}
|
|
|
|
private object publish {
|
|
private val debouncer = system.actorOf(Props(new Debouncer(15 seconds, { (_: Debouncer.Nothing) =>
|
|
fetchVisibleTournaments flatMap apiJsonView.apply foreach { json =>
|
|
Bus.publish(
|
|
SendToFlag("tournament", Json.obj("t" -> "reload", "d" -> json)),
|
|
"sendToFlag"
|
|
)
|
|
}
|
|
tournamentRepo.promotable foreach { tours =>
|
|
renderer.actor ? Tournament.TournamentTable(tours) map {
|
|
case view: String => Bus.publish(ReloadTournaments(view), "lobbySocket")
|
|
}
|
|
}
|
|
})))
|
|
def apply(): Unit = { debouncer ! Debouncer.Nothing }
|
|
}
|
|
|
|
private object updateTournamentStanding {
|
|
|
|
import lila.hub.EarlyMultiThrottler
|
|
|
|
// last published top hashCode
|
|
private val lastPublished = lila.memo.CacheApi.scaffeineNoScheduler
|
|
.initialCapacity(16)
|
|
.expireAfterWrite(2 minute)
|
|
.build[Tournament.ID, Int]
|
|
|
|
private def publishNow(tourId: Tournament.ID) = tournamentTop(tourId) map { top =>
|
|
val lastHash: Int = ~lastPublished.getIfPresent(tourId)
|
|
if (lastHash != top.hashCode) {
|
|
Bus.publish(
|
|
lila.hub.actorApi.round.TourStanding(tourId, JsonView.top(top, lightUserApi.sync)),
|
|
"tourStanding"
|
|
)
|
|
lastPublished.put(tourId, top.hashCode)
|
|
}
|
|
}
|
|
|
|
private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))
|
|
|
|
def apply(tourId: Tournament.ID): Unit =
|
|
throttler ! EarlyMultiThrottler.Work(
|
|
id = tourId,
|
|
run = () => publishNow(tourId),
|
|
cooldown = 15.seconds
|
|
)
|
|
}
|
|
}
|
|
|
|
private object TournamentApi {
|
|
|
|
case class Callbacks(
|
|
clearJsonViewCache: Tournament => Unit,
|
|
clearWinnersCache: Tournament => Unit,
|
|
clearTrophyCache: Tournament => Unit,
|
|
indexLeaderboard: Tournament => Funit
|
|
)
|
|
}
|