Lot of work just got done

pull/1/merge
Thibault Duplessis 2012-03-24 01:42:50 +01:00
parent 75f416f6aa
commit 9dac236e3d
18 changed files with 287 additions and 58 deletions

View File

@ -15,6 +15,17 @@ final class Cron(env: SystemEnv)(implicit app: Application) {
env.userRepo updateOnlineUsernames env.usernameMemo.keys
}
spawn("hook_cleanup_dead") { env
for {
hasRemoved env.hookRepo keepOnlyIds env.hookMemo.keys
_ if (hasRemoved) env.lobbyMemo++ else io()
} yield ()
}
spawn("hook_cleanup_old") { env
env.hookRepo.cleanupOld
}
def spawn(name: String)(f: SystemEnv IO[Unit]) = {
val freq = env.getMilliseconds("cron.online_username.frequency") millis
val actor = Akka.system.actorOf(Props(new Actor {

View File

@ -1,5 +1,6 @@
package lila.http
import lila.system.model._
import play.api.data._
import play.api.data.Forms._
@ -21,16 +22,20 @@ object DataForm {
"message" -> nonEmptyText
))
type JoinData = (String, String)
val entryGameForm = Form(entryGameMapping)
type JoinData = (String, String, EntryGame)
val joinForm = Form(tuple(
"redirect" -> nonEmptyText,
"messages" -> nonEmptyText
"messages" -> nonEmptyText,
"entry" -> entryGameMapping
))
type RematchData = (String, String)
type RematchData = (String, String, EntryGame)
val rematchForm = Form(tuple(
"whiteRedirect" -> nonEmptyText,
"blackRedirect" -> nonEmptyText
"blackRedirect" -> nonEmptyText,
"entry" -> entryGameMapping
))
private type MessagesData = String
@ -43,4 +48,15 @@ object DataForm {
type DrawData = MessagesData
val drawForm = messagesForm
private val entryGameMapping = mapping(
"id" -> nonEmptyText,
"players" -> list(mapping(
"u" -> optional(nonEmptyText),
"ue" -> nonEmptyText
)(EntryPlayer.apply)(EntryPlayer.unapply)),
"variant" -> nonEmptyText,
"rated" -> boolean,
"clock" -> list(number)
)(EntryGame.apply)(EntryGame.unapply)
}

View File

@ -1,5 +1,6 @@
package controllers
import lila.system.model.EntryGame
import lila.http._
import DataForm._
@ -26,11 +27,11 @@ object AppApiC extends LilaController {
IOk(api.alive(gameId, color))
}
def draw(gameId: String, color: String) = Action { implicit request =>
def draw(gameId: String, color: String) = Action { implicit request
ValidIOk[String](drawForm)(msgs api.draw(gameId, color, msgs))
}
def drawAccept(gameId: String, color: String) = Action { implicit request =>
def drawAccept(gameId: String, color: String) = Action { implicit request
ValidIOk[String](drawForm)(msgs api.drawAccept(gameId, color, msgs))
}
@ -38,16 +39,22 @@ object AppApiC extends LilaController {
ValidIOk[String](endForm)(msgs api.end(gameId, msgs))
}
def start = Action { implicit request =>
ValidIOk[EntryGame](entryGameForm)(entryGame api.start(entryGame))
}
def join(fullId: String) = Action { implicit request
ValidIOk[JoinData](joinForm)(join api.join(fullId, join._1, join._2))
ValidIOk[JoinData](joinForm) { join
api.join(fullId, join._1, join._2, join._3)
}
}
def activity(gameId: String, color: String) = Action {
Ok(api.activity(gameId, color).toString)
}
def acceptRematch(gameId: String, color: String, newGameId: String) = Action { implicit request
ValidIOk[RematchData](rematchForm)(rematch
api.acceptRematch(gameId, newGameId, color, rematch._1, rematch._2))
def rematchAccept(gameId: String, color: String, newGameId: String) = Action { implicit request
ValidIOk[RematchData](rematchForm)(r
api.acceptRematch(gameId, newGameId, color, r._1, r._2, r._3))
}
}

View File

@ -1,7 +1,7 @@
package controllers
import lila.system.model.{ Hook, EntryGame }
import lila.http._
import lila.system.model.Hook
import DataForm._
import play.api._
@ -11,12 +11,8 @@ object LobbyApiC extends LilaController {
private val api = env.lobbyApi
def join(gameId: String, color: String) = Action {
IOk(api.join(gameId, color))
}
def inc = Action {
IOk(api.inc)
def join(gameId: String, color: String) = Action { implicit request
ValidIOk[EntryGame](entryGameForm)(ec api.join(gameId, color, ec))
}
def create(hookOwnerId: String) = Action {
@ -26,4 +22,8 @@ object LobbyApiC extends LilaController {
def remove(hookId: String) = Action {
IOk(api.remove(hookId))
}
def alive(hookOwnerId: String) = Action {
IOk(api.alive(hookOwnerId))
}
}

View File

@ -19,7 +19,8 @@ object LobbyXhrC extends LilaController {
JsonOk(syncer.sync(
hookId,
getIntOr("auth", 0) == 1,
getIntOr("state", 0)
getIntOr("state", 0),
getIntOr("entryId", 0)
).unsafePerformIO)
}
}

View File

@ -6,6 +6,7 @@ mongo {
game = game2
user = user
hook = hook
entry = lobby_entry
}
}
redis {
@ -19,7 +20,9 @@ sync {
lobby {
sync {
duration = 10 seconds
sleep = 300 milliseconds
#duration = 2 seconds
sleep = 250 milliseconds
max_entries = 12
}
}
memo {
@ -36,6 +39,8 @@ crafty {
}
cron {
online_username.frequency = 2 seconds
hook_cleanup_dead.frequency = 2 seconds
hook_cleanup_old.frequency = 21 seconds
}
akka {
debug.receive = on

View File

@ -3,19 +3,20 @@
# ~~~~
# App XHR
POST /move/:fullId controllers.AppXhrC.move(fullId: String)
GET /ping controllers.AppXhrC.ping
GET /sync/:gameId/:color/:version controllers.AppXhrC.syncPublic(gameId: String, color: String, version: Int)
GET /sync/:gameId/:color/:version/:fullId controllers.AppXhrC.sync(gameId: String, color: String, version: Int, fullId: String)
GET /ping controllers.AppXhrC.ping
GET /how-many-players-now controllers.AppXhrC.nbPlayers
POST /move/:fullId controllers.AppXhrC.move(fullId: String)
# App Private API
POST /api/update-version/:gameId controllers.AppApiC.updateVersion(gameId: String)
POST /api/start controllers.AppApiC.start
POST /api/end/:gameId controllers.AppApiC.end(gameId: String)
POST /api/talk/:fullId controllers.AppApiC.talk(fullId: String)
POST /api/join/:fullId controllers.AppApiC.join(fullId: String)
POST /api/reload-table/:gameId controllers.AppApiC.reloadTable(gameId: String)
POST /api/accept-rematch/:gameId/:color/:newGameId controllers.AppApiC.acceptRematch(gameId: String, color: String, newGameId: String)
POST /api/rematch-accept/:gameId/:color/:newGameId controllers.AppApiC.rematchAccept(gameId: String, color: String, newGameId: String)
POST /api/alive/:gameId/:color controllers.AppApiC.alive(gameId: String, color: String)
POST /api/draw/:gameId/:color controllers.AppApiC.draw(gameId: String, color: String)
POST /api/draw-accept/:gameId/:color controllers.AppApiC.drawAccept(gameId: String, color: String)
@ -30,9 +31,9 @@ GET /lobby/sync controllers.LobbyXhrC.syncWithoutHook
POST /api/lobby/join/:gameId/:color controllers.LobbyApiC.join(gameId: String, color: String)
GET /api/lobby/preload/:hookId controllers.LobbyXhrC.syncWithHook(hookId: String)
GET /api/lobby/preload controllers.LobbyXhrC.syncWithoutHook
POST /api/lobby/inc controllers.LobbyApiC.inc
POST /api/lobby/create/:hookOwnerId controllers.LobbyApiC.create(hookOwnerId: String)
POST /api/lobby/remove/:hookId controllers.LobbyApiC.remove(hookId: String)
POST /api/lobby/alive/:hookOwnerId controllers.LobbyApiC.alive(hookOwnerId: String)
# Useless, but play2 needs it
GET /assets/*file controllers.Assets.at(path="/public", file)

View File

@ -7,7 +7,6 @@ trait Resolvers {
val typesafe = "typesafe.com" at "http://repo.typesafe.com/typesafe/releases/"
val iliaz = "iliaz.com" at "http://scala.iliaz.com/"
val sonatype = "sonatype" at "http://oss.sonatype.org/content/repositories/releases"
val novusS = "repo.novus snaps" at "http://repo.novus.com/snapshots/"
}
trait Dependencies {
@ -21,6 +20,9 @@ trait Dependencies {
val json = "com.codahale" %% "jerkson" % "0.5.0"
val guava = "com.google.guava" % "guava" % "11.0.2"
val apache = "org.apache.commons" % "commons-lang3" % "3.1"
val jodaTime = "joda-time" % "joda-time" % "2.0"
val jodaConvert = "org.joda" % "joda-convert" % "1.2"
val scalaTime = "org.scala-tools.time" %% "time" % "0.5"
// benchmark
val instrumenter = "com.google.code.java-allocation-instrumenter" % "java-allocation-instrumenter" % "2.0"
@ -33,7 +35,7 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
organization := "com.github.ornicar",
version := "0.1",
scalaVersion := "2.9.1",
resolvers := Seq(iliaz, codahale, sonatype, novusS, typesafe),
resolvers := Seq(iliaz, codahale, sonatype, typesafe),
libraryDependencies := Seq(scalalib),
libraryDependencies in test := Seq(specs2),
shellPrompt := {
@ -54,7 +56,7 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
) dependsOn (system)
lazy val system = Project("system", file("system"), settings = buildSettings).settings(
libraryDependencies ++= Seq(scalaz, config, json, casbah, salat, guava, apache)
libraryDependencies ++= Seq(scalaz, config, json, casbah, salat, guava, apache, jodaTime, jodaConvert, scalaTime)
) dependsOn (chess)
lazy val chess = Project("chess", file("chess"), settings = buildSettings).settings(

View File

@ -9,14 +9,20 @@ import scalaz.effects._
case class AppApi(
gameRepo: GameRepo,
versionMemo: VersionMemo,
aliveMemo: AliveMemo) extends IOTools {
aliveMemo: AliveMemo,
addEntry: EntryGame IO[Unit]) extends IOTools {
def join(fullId: String, url: String, messages: String): IO[Unit] = for {
def join(
fullId: String,
url: String,
messages: String,
entryGame: EntryGame): IO[Unit] = for {
gameAndPlayer gameRepo player fullId
(g1, player) = gameAndPlayer
g2 = g1 withEvents decodeMessages(messages)
g3 = g2.withEvents(g2.opponent(player).color, List(RedirectEvent(url)))
_ save(g1, g3)
_ addEntry(entryGame)
} yield ()
def talk(gameId: String, author: String, message: String): IO[Unit] = for {
@ -31,12 +37,15 @@ case class AppApi(
_ save(g1, g2)
} yield ()
def start(entryGame: EntryGame): IO[Unit] = addEntry(entryGame)
def acceptRematch(
gameId: String,
newGameId: String,
colorName: String,
whiteRedirect: String,
blackRedirect: String): IO[Unit] = for {
blackRedirect: String,
entryGame: EntryGame): IO[Unit] = for {
color ioColor(colorName)
g1 gameRepo game gameId
g2 = g1.withEvents(
@ -45,6 +54,7 @@ case class AppApi(
_ save(g1, g2)
_ aliveMemo.put(newGameId, !color)
_ aliveMemo.transfer(gameId, !color, newGameId, color)
_ addEntry(entryGame)
} yield ()
def updateVersion(gameId: String): IO[Unit] =

View File

@ -2,34 +2,47 @@ package lila.system
import model._
import memo._
import db.{ GameRepo, HookRepo }
import db._
import scalaz.effects._
case class LobbyApi(
hookRepo: HookRepo,
lobbyMemo: LobbyMemo,
versionMemo: VersionMemo,
gameRepo: GameRepo,
entryRepo: EntryRepo,
lobbyMemo: LobbyMemo,
entryMemo: EntryMemo,
versionMemo: VersionMemo,
aliveMemo: AliveMemo,
hookMemo: HookMemo) extends IOTools {
def join(gameId: String, colorName: String): IO[Unit] = for {
def join(
gameId: String,
colorName: String,
entryGame: EntryGame): IO[Unit] = for {
color ioColor(colorName)
g1 gameRepo game gameId
_ aliveMemo.put(gameId, color)
_ aliveMemo.put(gameId, !color)
_ (lobbyMemo++)
_ versionInc
_ addEntry(entryGame)
} yield ()
def inc: IO[Unit] = lobbyMemo++
def create(hookOwnerId: String): IO[Unit] = for {
_ (lobbyMemo++)
_ versionInc
_ hookMemo put hookOwnerId
} yield ()
def remove(hookId: String): IO[Unit] = for {
_ hookRepo removeId hookId
_ (lobbyMemo++)
_ versionInc
} yield ()
def alive(hookOwnerId: String): IO[Unit] = hookMemo put hookOwnerId
private[system] def versionInc: IO[Int] = lobbyMemo++
private[system] def addEntry(entryGame: EntryGame): IO[Unit] = for {
nextId (entryMemo++)
_ io { entryRepo insert Entry(nextId, entryGame) }
} yield ()
}

View File

@ -4,38 +4,45 @@ import model._
import memo._
import db._
import scalaz.effects._
import scalaz.NonEmptyList
import scala.annotation.tailrec
import scala.math.max
final class LobbySyncer(
hookRepo: HookRepo,
gameRepo: GameRepo,
entryRepo: EntryRepo,
lobbyMemo: LobbyMemo,
hookMemo: HookMemo,
entryMemo: EntryMemo,
duration: Int,
sleep: Int) {
sleep: Int,
maxEntries: Int) {
type Response = Map[String, Any]
def sync(
myHookId: Option[String],
auth: Boolean,
version: Int): IO[Response] = for {
newVersion versionWait(version)
version: Int,
entryId: Int): IO[Response] = for {
_ myHookId.fold(hookMemo.put, io())
newVersion wait(version, entryId)
hooks if (auth) hookRepo.allOpen else hookRepo.allOpenCasual
response {
val response = () stdResponse(newVersion, hooks, myHookId)
myHookId some { hookResponse(_, response) } none io { response() }
res {
val response = () stdResponse(newVersion, hooks, myHookId, entryId)
myHookId some { hookResponse(_, response) } none response()
}
} yield response
} yield res
def hookResponse(myHookId: String, response: () Response): IO[Response] =
def hookResponse(myHookId: String, response: () IO[Response]): IO[Response] =
hookRepo ownedHook myHookId flatMap { hookOption
hookOption.fold(
hook hook.game.fold(
ref gameRepo game ref.getId.toString.pp map { game
Map("redirect" -> (game fullIdOf game.creatorColor))
},
io { response() }
response()
),
io { Map("redirect" -> "") }
)
@ -44,30 +51,56 @@ final class LobbySyncer(
def stdResponse(
version: Int,
hooks: List[Hook],
myHookId: Option[String]): Response = Map(
myHookId: Option[String],
entryId: Int): IO[Response] = for {
entries
if (entryId == 0) entryRepo recent maxEntries
else entryRepo since max(entryMemo.id - maxEntries, entryId)
} yield Map(
"state" -> version,
"pool" -> {
if (hooks.nonEmpty) Map("hooks" -> renderHooks(hooks, myHookId).toMap)
else Map("message" -> "No game available right now, create one!")
},
"chat" -> null,
"timeline" -> ""
"timeline" -> entries.toNel.fold(
renderTimeline,
Map("id" -> entryId, "entries" -> Nil)
)
)
private def renderTimeline(entries: NonEmptyList[Entry]) = Map(
"id" -> entries.head.id,
"entries" -> (entries.list.reverse map { entry =>
"<td>%s</td><td>%s</td><td class='trans_me'>%s</td><td>%s</td><td class='trans_me'>%s</td>".format(
"<a class='watch' href='/%s'></a>" format entry.data.id,
entry.data.players map { p
p.u.fold(
username "<a class='user_link' href='/@/%s'>%s</a>".format(username, p.ue),
p.ue)
} mkString " vs ",
entry.data.variant,
entry.data.rated ? "Rated" | "Casual",
entry.data.clock |> { c if (c.empty) "Unlimited" else c mkString " + " }
)
})
)
private def renderHooks(hooks: List[Hook], myHookId: Option[String]) = for {
hook hooks
} yield hook.id -> {
hook.render ++ {
if (myHookId == Some(hook.ownerId))
Map("action" -> "cancel", "id" -> myHookId)
if (myHookId == Some(hook.ownerId)) Map("action" -> "cancel", "id" -> myHookId)
else Map("action" -> "join", "id" -> hook.id)
}
}
private def versionWait(version: Int): IO[Int] = io {
private def wait(version: Int, entryId: Int): IO[Int] = io {
@tailrec
def wait(loop: Int): Int = {
if (loop == 0 || lobbyMemo.version != version) lobbyMemo.version
if (loop == 0 ||
lobbyMemo.version != version ||
entryMemo.id != entryId) lobbyMemo.version
else { Thread sleep sleep; wait(loop - 1) }
}
wait(max(1, duration / sleep))

View File

@ -18,7 +18,8 @@ final class SystemEnv(config: Config) {
lazy val appApi = new AppApi(
gameRepo = gameRepo,
versionMemo = versionMemo,
aliveMemo = aliveMemo)
aliveMemo = aliveMemo,
addEntry = lobbyApi.addEntry)
lazy val appSyncer = new AppSyncer(
gameRepo = gameRepo,
@ -34,18 +35,24 @@ final class SystemEnv(config: Config) {
lazy val lobbyApi = new LobbyApi(
hookRepo = hookRepo,
gameRepo = gameRepo,
entryRepo = entryRepo,
versionMemo = versionMemo,
lobbyMemo = lobbyMemo,
gameRepo = gameRepo,
entryMemo = entryMemo,
aliveMemo = aliveMemo,
hookMemo = hookMemo)
lazy val lobbySyncer = new LobbySyncer(
hookRepo = hookRepo,
gameRepo = gameRepo,
entryRepo = entryRepo,
lobbyMemo = lobbyMemo,
hookMemo = hookMemo,
entryMemo = entryMemo,
duration = getMilliseconds("lobby.sync.duration"),
sleep = getMilliseconds("lobby.sync.sleep"))
sleep = getMilliseconds("lobby.sync.sleep"),
maxEntries = config getInt "lobby.sync.max_entries")
lazy val pinger = new Pinger(
aliveMemo = aliveMemo,
@ -58,7 +65,6 @@ final class SystemEnv(config: Config) {
execPath = config getString "crafty.exec_path",
bookPath = Some(config getString "crafty.book_path") filter ("" !=))
lazy val gameRepo = new GameRepo(
mongodb(config getString "mongo.collection.game"))
@ -68,6 +74,9 @@ final class SystemEnv(config: Config) {
lazy val hookRepo = new HookRepo(
mongodb(config getString "mongo.collection.hook"))
lazy val entryRepo = new EntryRepo(
mongodb(config getString "mongo.collection.entry"))
lazy val mongodb = MongoConnection(
config getString "mongo.host",
config getInt "mongo.port"
@ -92,6 +101,9 @@ final class SystemEnv(config: Config) {
lazy val hookMemo = new HookMemo(
timeout = getMilliseconds("memo.hook.timeout"))
lazy val entryMemo = new EntryMemo(
getId = entryRepo.lastId)
def getMilliseconds(name: String): Int = (config getMilliseconds name).toInt
}

View File

@ -0,0 +1,33 @@
package lila.system
package db
import model.Entry
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
class EntryRepo(collection: MongoCollection)
extends SalatDAO[Entry, String](collection) {
private val idSelector = DBObject("_id" -> true)
private val idSorter = DBObject("_id" -> -1)
val lastId: () IO[Option[Int]] = () io {
collection.find(DBObject(), idSelector)
.sort(idSorter)
.limit(1)
.next()
.getAs[Int]("_id")
}
def recent(max: Int) = io {
find(DBObject()).sort(idSorter).limit(max).toList
}
def since(id: Int): IO[List[Entry]] = io {
find("_id" $gt id).sort(idSorter).toList
}
}

View File

@ -8,6 +8,8 @@ import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
import org.joda.time.DateTime
import org.scala_tools.time.Imports._
class HookRepo(collection: MongoCollection)
extends SalatDAO[Hook, String](collection) {
@ -36,4 +38,38 @@ class HookRepo(collection: MongoCollection)
def removeId(id: String): IO[Unit] = io {
remove(DBObject("id" -> id))
}
def keepOnlyIds(ids: Iterable[String]): IO[Boolean] = io {
val removableIds = collection.find(
("_id" $nin ids) ++ ("match" -> false),
DBObject("_id" -> true)
).toList
if (removableIds.nonEmpty) {
remove("_id" $in removableIds)
true
}
else false
}
def cleanupOld: IO[Unit] = io {
remove("createdAt" $lt (DateTime.now - 1.hour))
}
//public function removeDeadHooks()
//{
//if (0 == time()%10) {
//$this->hookRepository->removeOldHooks();
//}
//$hooks = $this->hookRepository->findAllOpen();
//$removed = false;
//foreach ($hooks as $hook) {
//if (!$this->memory->isAlive($hook)) {
//$this->hookRepository->getDocumentManager()->remove($hook);
//$removed = true;
//}
//}
//if ($removed) {
//$this->memory->incrementState();
//}
//}
}

View File

@ -0,0 +1,26 @@
package lila.system
package memo
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
final class EntryMemo(getId: () => IO[Option[Int]]) {
private var privateId: Int = _
refresh.unsafePerformIO
def refresh = for {
idOption getId()
} yield {
privateId = idOption err "No last entry found"
}
def ++ : IO[Int] = io {
privateId = privateId + 1
privateId
}
def id: Int = privateId
}

View File

@ -9,7 +9,8 @@ final class LobbyMemo {
def version: Int = privateVersion
def ++ : IO[Unit] = io {
def ++ : IO[Int] = io {
privateVersion = privateVersion + 1
privateVersion
}
}

View File

@ -0,0 +1,18 @@
package lila.system
package model
import com.novus.salat.annotations._
case class Entry(
@Key("_id") id: Int,
data: EntryGame) {
}
case class EntryPlayer(u: Option[String], ue: String)
case class EntryGame(
id: String,
players: List[EntryPlayer],
variant: String,
rated: Boolean,
clock: List[Int] = Nil)

View File

@ -3,14 +3,18 @@ package lila
import ornicar.scalalib._
import com.novus.salat._
import com.mongodb.casbah.commons.conversions.scala.RegisterJodaTimeConversionHelpers
package object system
extends OrnicarValidation
with OrnicarCommon
with scalaz.NonEmptyLists
with scalaz.Strings
with scalaz.Lists
with scalaz.Booleans {
RegisterJodaTimeConversionHelpers()
// custom salat context
implicit val ctx = new Context {
val name = "Lila System Context"