2016-03-12 05:56:44 -07:00
|
|
|
package lila.fishnet
|
|
|
|
|
2020-08-17 12:47:23 -06:00
|
|
|
import org.joda.time.DateTime
|
2020-01-14 19:30:51 -07:00
|
|
|
import scala.concurrent.duration._
|
2016-03-12 05:56:44 -07:00
|
|
|
|
2016-03-18 02:11:00 -06:00
|
|
|
import lila.analyse.AnalysisRepo
|
2019-12-08 09:58:50 -07:00
|
|
|
import lila.game.{ Game, UciMemo }
|
2016-03-12 05:56:44 -07:00
|
|
|
|
|
|
|
final class Analyser(
|
2016-03-12 23:59:06 -07:00
|
|
|
repo: FishnetRepo,
|
2019-12-01 19:04:35 -07:00
|
|
|
analysisRepo: AnalysisRepo,
|
|
|
|
gameRepo: lila.game.GameRepo,
|
2016-03-12 05:56:44 -07:00
|
|
|
uciMemo: UciMemo,
|
2017-11-24 22:13:52 -07:00
|
|
|
evalCache: FishnetEvalCache,
|
2021-04-12 01:05:18 -06:00
|
|
|
limiter: FishnetLimiter
|
2020-06-24 03:37:18 -06:00
|
|
|
)(implicit
|
|
|
|
ec: scala.concurrent.ExecutionContext,
|
|
|
|
system: akka.actor.ActorSystem
|
|
|
|
) {
|
2016-03-12 05:56:44 -07:00
|
|
|
|
2021-12-11 08:44:16 -07:00
|
|
|
val maxPlies = 300
|
2016-03-12 10:00:27 -07:00
|
|
|
|
2021-07-24 00:54:43 -06:00
|
|
|
private val workQueue =
|
|
|
|
new lila.hub.AsyncActorSequencer(maxSize = 256, timeout = 5 seconds, "fishnetAnalyser")
|
2019-12-09 16:24:43 -07:00
|
|
|
|
2021-10-15 22:51:30 -06:00
|
|
|
def apply(game: Game, sender: Work.Sender): Fu[Analyser.Result] =
|
2019-12-01 19:04:35 -07:00
|
|
|
(game.metadata.analysed ?? analysisRepo.exists(game.id)) flatMap {
|
2021-10-15 22:51:30 -06:00
|
|
|
case true => fuccess(Analyser.Result.AlreadyAnalysed)
|
|
|
|
case _ if !game.analysable => fuccess(Analyser.Result.NotAnalysable)
|
2016-09-04 11:07:21 -06:00
|
|
|
case _ =>
|
2021-04-12 01:05:18 -06:00
|
|
|
limiter(
|
|
|
|
sender,
|
|
|
|
ignoreConcurrentCheck = false,
|
|
|
|
ownGame = game.userIds contains sender.userId
|
2021-10-15 22:51:30 -06:00
|
|
|
) flatMap { result =>
|
|
|
|
result.ok ?? {
|
2016-03-18 02:11:00 -06:00
|
|
|
makeWork(game, sender) flatMap { work =>
|
2019-12-09 16:24:43 -07:00
|
|
|
workQueue {
|
2016-03-18 02:11:00 -06:00
|
|
|
repo getSimilarAnalysis work flatMap {
|
|
|
|
// already in progress, do nothing
|
|
|
|
case Some(similar) if similar.isAcquired => funit
|
|
|
|
// queued by system, reschedule for the human sender
|
|
|
|
case Some(similar) if similar.sender.system && !sender.system =>
|
|
|
|
repo.updateAnalysis(similar.copy(sender = sender))
|
|
|
|
// queued for someone else, do nothing
|
2019-12-08 09:58:50 -07:00
|
|
|
case Some(_) => funit
|
2016-03-18 02:11:00 -06:00
|
|
|
// first request, store
|
2016-11-21 09:18:33 -07:00
|
|
|
case _ =>
|
2019-12-10 14:01:18 -07:00
|
|
|
lila.mon.fishnet.analysis.requestCount("game").increment()
|
2017-11-25 04:38:24 -07:00
|
|
|
evalCache skipPositions work.game flatMap { skipPositions =>
|
2020-01-03 21:20:08 -07:00
|
|
|
lila.mon.fishnet.analysis.evalCacheHits.record(skipPositions.size)
|
2017-11-25 20:44:01 -07:00
|
|
|
repo addAnalysis work.copy(skipPositions = skipPositions)
|
2017-11-23 20:39:34 -07:00
|
|
|
}
|
2016-03-18 02:11:00 -06:00
|
|
|
}
|
|
|
|
}
|
2016-03-12 05:56:44 -07:00
|
|
|
}
|
2021-10-15 22:51:30 -06:00
|
|
|
} inject result
|
2016-03-12 05:56:44 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-15 22:51:30 -06:00
|
|
|
def apply(gameId: String, sender: Work.Sender): Fu[Analyser.Result] =
|
|
|
|
gameRepo game gameId flatMap {
|
|
|
|
_.fold[Fu[Analyser.Result]](fuccess(Analyser.Result.NoGame))(apply(_, sender))
|
|
|
|
}
|
2016-03-13 00:04:01 -07:00
|
|
|
|
2021-10-15 22:51:30 -06:00
|
|
|
def study(req: lila.hub.actorApi.fishnet.StudyChapterRequest): Fu[Analyser.Result] =
|
2019-12-01 19:04:35 -07:00
|
|
|
analysisRepo exists req.chapterId flatMap {
|
2021-10-15 22:51:30 -06:00
|
|
|
case true => fuccess(Analyser.Result.NoChapter)
|
2020-08-16 06:48:46 -06:00
|
|
|
case _ =>
|
2018-01-15 21:12:10 -07:00
|
|
|
import req._
|
2020-11-08 15:51:26 -07:00
|
|
|
val sender = Work.Sender(req.userId, none, mod = false, system = false)
|
2021-10-15 22:51:30 -06:00
|
|
|
(if (req.unlimited) fuccess(Analyser.Result.Ok)
|
|
|
|
else limiter(sender, ignoreConcurrentCheck = true, ownGame = false)) flatMap { result =>
|
|
|
|
if (!result.ok) logger.info(s"Study request declined: ${req.studyId}/${req.chapterId} by $sender")
|
|
|
|
result.ok ?? {
|
|
|
|
val work = makeWork(
|
|
|
|
game = Work.Game(
|
|
|
|
id = chapterId,
|
|
|
|
initialFen = initialFen,
|
|
|
|
studyId = studyId.some,
|
|
|
|
variant = variant,
|
|
|
|
moves = moves take maxPlies map (_.uci) mkString " "
|
|
|
|
),
|
|
|
|
// if black moves first, use 1 as startPly so the analysis doesn't get reversed
|
|
|
|
startPly = initialFen.flatMap(_.color).??(_.fold(0, 1)),
|
|
|
|
sender = sender
|
|
|
|
)
|
|
|
|
workQueue {
|
|
|
|
repo getSimilarAnalysis work flatMap {
|
|
|
|
_.isEmpty ?? {
|
|
|
|
lila.mon.fishnet.analysis.requestCount("study").increment()
|
|
|
|
evalCache skipPositions work.game flatMap { skipPositions =>
|
|
|
|
lila.mon.fishnet.analysis.evalCacheHits.record(skipPositions.size)
|
|
|
|
repo addAnalysis work.copy(skipPositions = skipPositions)
|
2018-01-15 21:12:10 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-10-15 22:51:30 -06:00
|
|
|
}
|
|
|
|
} inject result
|
2018-01-15 21:12:10 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-12 05:56:44 -07:00
|
|
|
private def makeWork(game: Game, sender: Work.Sender): Fu[Work.Analysis] =
|
2020-09-21 01:28:28 -06:00
|
|
|
gameRepo.initialFen(game) zip uciMemo.get(game) map { case (initialFen, moves) =>
|
|
|
|
makeWork(
|
|
|
|
game = Work.Game(
|
|
|
|
id = game.id,
|
|
|
|
initialFen = initialFen,
|
|
|
|
studyId = none,
|
|
|
|
variant = game.variant,
|
|
|
|
moves = moves take maxPlies mkString " "
|
|
|
|
),
|
|
|
|
startPly = game.chess.startedAtTurn,
|
|
|
|
sender = sender
|
|
|
|
)
|
2016-03-12 05:56:44 -07:00
|
|
|
}
|
2018-01-15 21:12:10 -07:00
|
|
|
|
|
|
|
private def makeWork(game: Work.Game, startPly: Int, sender: Work.Sender): Work.Analysis =
|
|
|
|
Work.Analysis(
|
|
|
|
_id = Work.makeId,
|
|
|
|
sender = sender,
|
|
|
|
game = game,
|
|
|
|
startPly = startPly,
|
|
|
|
tries = 0,
|
|
|
|
lastTryByKey = none,
|
|
|
|
acquired = none,
|
|
|
|
skipPositions = Nil,
|
|
|
|
createdAt = DateTime.now
|
|
|
|
)
|
2016-03-12 05:56:44 -07:00
|
|
|
}
|
2021-10-15 22:51:30 -06:00
|
|
|
|
|
|
|
object Analyser {
|
|
|
|
|
|
|
|
sealed abstract class Result(val error: Option[String]) {
|
|
|
|
def ok = error.isEmpty
|
|
|
|
}
|
|
|
|
object Result {
|
|
|
|
case object Ok extends Result(none)
|
|
|
|
case object NoGame extends Result("Game not found".some)
|
|
|
|
case object NoChapter extends Result("Chapter not found".some)
|
|
|
|
case object AlreadyAnalysed extends Result("This game is already analysed".some)
|
|
|
|
case object NotAnalysable extends Result("This game is not analysable".some)
|
|
|
|
case object ConcurrentAnalysis extends Result("You already have an ongoing requested analysis".some)
|
2021-10-16 00:17:04 -06:00
|
|
|
case object WeeklyLimit extends Result("You have reached the weekly analysis limit".some)
|
|
|
|
case object DailyLimit extends Result("You have reached the daily analysis limit".some)
|
|
|
|
case object DailyIpLimit extends Result("You have reached the daily analysis limit on this IP".some)
|
2021-10-15 22:51:30 -06:00
|
|
|
}
|
|
|
|
}
|