From 177991ec0075175d2c7e2ca1e98816412af670a1 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 30 Mar 2016 18:10:36 +0700 Subject: [PATCH 01/26] upgrade to play 2.5.1 WIP --- conf/logback.xml | 2 +- project/Build.scala | 4 +++- project/BuildSettings.scala | 2 +- project/Dependencies.scala | 2 +- project/plugins.sbt | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/conf/logback.xml b/conf/logback.xml index 4c9e1dd4f5..ae988ca169 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -1,6 +1,6 @@ - + diff --git a/project/Build.scala b/project/Build.scala index 7bcd658085..4a476bc7b5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1,7 +1,8 @@ import com.typesafe.sbt.packager.Keys.scriptClasspath import com.typesafe.sbt.web.SbtWeb.autoImport._ -import play.Play.autoImport._ +import play.sbt.Play.autoImport._ import play.sbt.PlayImport._ +import play.sbt.routes.RoutesKeys._ import play.twirl.sbt.Import._ import PlayKeys._ import sbt._, Keys._ @@ -21,6 +22,7 @@ object ApplicationBuild extends Build { scalacOptions := compilerOptions, incOptions := incOptions.value.withNameHashing(true), updateOptions := updateOptions.value.withCachedResolution(true), + routesGenerator := StaticRoutesGenerator, sources in doc in Compile := List(), // disable publishing the main API jar publishArtifact in (Compile, packageDoc) := false, diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 3b1851d648..e696d4aa5e 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -1,4 +1,4 @@ -import play.Play.autoImport._ +import play.sbt.Play.autoImport._ import sbt._, Keys._ object BuildSettings { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f9bb25f3f9..a8ca32ef03 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -43,7 +43,7 @@ object Dependencies { val semver = "com.gilt" %% "gfc-semver" % "0.0.2-9-g11173e1" object play { - val version = "2.4.6" + val version = "2.5.1" val api = "com.typesafe.play" %% "play" % version val test = "com.typesafe.play" %% "play-test" % version } diff --git a/project/plugins.sbt b/project/plugins.sbt index 6a8deb5e3a..ec045af777 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,3 @@ resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.6") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.1") From af189d1bc22c8d6fa9ebf0debef08492f5189cbc Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 31 Mar 2016 14:00:31 +0700 Subject: [PATCH 02/26] only recompile messages when needed --- project/MessageCompiler.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/project/MessageCompiler.scala b/project/MessageCompiler.scala index 5ba3f536cf..bb7d29052d 100644 --- a/project/MessageCompiler.scala +++ b/project/MessageCompiler.scala @@ -19,9 +19,11 @@ object MessageCompiler { val (lang, file) = entry val srcFile = src / file val dstFile = dst / s"$lang.scala" - val pairs = readLines(srcFile) map makePair - printToFile(dstFile) { - render(lang, pairs) + if (srcFile.lastModified > dstFile.lastModified) { + val pairs = readLines(srcFile) map makePair + printToFile(dstFile) { + render(lang, pairs) + } } dstFile } From 1eb2fcd3cb4593330e4b63472bd59d1f8857b5d3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 1 Apr 2016 11:17:37 +0700 Subject: [PATCH 03/26] play 2.5 WS migration WIP --- modules/site/src/main/Socket.scala | 2 +- modules/site/src/main/actorApi.scala | 2 +- modules/socket/src/main/Handler.scala | 16 +++++++++++++- modules/socket/src/main/SocketMember.scala | 17 ++++++-------- .../socket/src/main/SocketMemberActor.scala | 22 +++++++++++++++++++ modules/socket/src/main/WithSocket.scala | 7 ++++++ project/Build.scala | 5 +++-- project/Dependencies.scala | 1 + 8 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 modules/socket/src/main/SocketMemberActor.scala diff --git a/modules/site/src/main/Socket.scala b/modules/site/src/main/Socket.scala index 69e7fece13..72bf7a2ed5 100644 --- a/modules/site/src/main/Socket.scala +++ b/modules/site/src/main/Socket.scala @@ -25,7 +25,7 @@ private[site] final class Socket(timeout: Duration) extends SocketActor[Member]( case SendToFlag(flag, message) => members.values filter (_ hasFlag flag) foreach { - _.channel push message + _ push message } } } diff --git a/modules/site/src/main/actorApi.scala b/modules/site/src/main/actorApi.scala index 7b1e0c0625..dda4be8d2d 100644 --- a/modules/site/src/main/actorApi.scala +++ b/modules/site/src/main/actorApi.scala @@ -6,7 +6,7 @@ import play.api.libs.json._ import lila.socket.SocketMember case class Member( - channel: JsChannel, + actor: akka.actor.ActorRef, userId: Option[String], flag: Option[String]) extends SocketMember { diff --git a/modules/socket/src/main/Handler.scala b/modules/socket/src/main/Handler.scala index ddae564bc9..091404b9f8 100644 --- a/modules/socket/src/main/Handler.scala +++ b/modules/socket/src/main/Handler.scala @@ -1,7 +1,10 @@ package lila.socket import akka.actor.ActorRef +import akka.NotUsed import akka.pattern.{ ask, pipe } +import akka.stream._ +import akka.stream.scaladsl._ import play.api.libs.iteratee.{ Iteratee, Enumerator } import play.api.libs.json._ import scala.concurrent.duration._ @@ -26,7 +29,7 @@ object Handler { socket: ActorRef, uid: String, join: Any, - userId: Option[String])(connecter: Connecter): Fu[JsSocketHandler] = { + userId: Option[String])(connecter: Connecter): Fu[JsFlow] = { def baseController(member: SocketMember): Controller = { case ("p", _) => socket ! Ping(uid) @@ -92,6 +95,17 @@ object Handler { ).map(_ => socket ! Quit(uid)) } + def sink(controller: Controller, member: SocketMember) = { + val control = controller orElse baseController(member) + Sink.foreach[JsValue] { jsv => + jsv.asOpt[JsObject] foreach { obj => + obj str "t" foreach { t => + control.lift(t -> obj) + } + } + } + } + socket ? join map connecter map { case (controller, enum, member) => iteratee(controller, member) -> enum } diff --git a/modules/socket/src/main/SocketMember.scala b/modules/socket/src/main/SocketMember.scala index 9d544729a4..4f1319eaf8 100644 --- a/modules/socket/src/main/SocketMember.scala +++ b/modules/socket/src/main/SocketMember.scala @@ -1,10 +1,11 @@ package lila.socket +import akka.actor.{ ActorRef, PoisonPill } import play.api.libs.json.JsValue trait SocketMember extends Ordered[SocketMember] { - protected val channel: JsChannel + protected val actor: ActorRef val userId: Option[String] val troll: Boolean @@ -12,21 +13,17 @@ trait SocketMember extends Ordered[SocketMember] { def compare(other: SocketMember) = ~userId compare ~other.userId - def push(msg: JsValue) { - channel push msg - } + def push(msg: JsValue) = actor ! msg - def end { - channel.end - } + def end = actor ! PoisonPill } object SocketMember { - def apply(c: JsChannel): SocketMember = apply(c, none, false) + def apply(a: ActorRef): SocketMember = apply(a, none, false) - def apply(c: JsChannel, uid: Option[String], tr: Boolean): SocketMember = new SocketMember { - val channel = c + def apply(a: ActorRef, uid: Option[String], tr: Boolean): SocketMember = new SocketMember { + val actor = a val userId = uid val troll = tr } diff --git a/modules/socket/src/main/SocketMemberActor.scala b/modules/socket/src/main/SocketMemberActor.scala new file mode 100644 index 0000000000..eadf65f6dd --- /dev/null +++ b/modules/socket/src/main/SocketMemberActor.scala @@ -0,0 +1,22 @@ +package lila.socket + +import akka.actor._ +import play.api.libs.json.JsValue + +final class SocketMemberActor(out: ActorRef) extends Actor { + + import SocketMemberActor._ + + def receive = { + + case Out(msg) => out ! msg + + case msg: JsValue => + // out ! ("I received your message: " + msg) + } +} + +object SocketMemberActor { + + case class Out(msg: JsValue) +} diff --git a/modules/socket/src/main/WithSocket.scala b/modules/socket/src/main/WithSocket.scala index 6467c900b3..2bac2478d8 100644 --- a/modules/socket/src/main/WithSocket.scala +++ b/modules/socket/src/main/WithSocket.scala @@ -4,6 +4,9 @@ import ornicar.scalalib.Zero import play.api.libs.iteratee.{ Iteratee, Enumerator } import play.api.libs.json._ +import akka.NotUsed +import akka.stream._ +import akka.stream.scaladsl._ trait WithSocket { @@ -11,4 +14,8 @@ trait WithSocket { type JsEnumerator = Enumerator[JsValue] type JsIteratee = Iteratee[JsValue, _] type JsSocketHandler = (JsIteratee, JsEnumerator) + + type JsFlow = Flow[JsValue, JsValue, NotUsed] + // type JsSource = Source[JsValue, NotUsed] + // type JsSink = Sink[JsValue, NotUsed] } diff --git a/project/Build.scala b/project/Build.scala index 7bcd658085..3524b8048e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -33,7 +33,8 @@ object ApplicationBuild extends Build { // offline := true, libraryDependencies ++= Seq( scalaz, scalalib, hasher, config, apache, - jgit, findbugs, RM, PRM, akka.actor, akka.slf4j, + jgit, findbugs, RM, PRM, + akka.actor, akka.slf4j, akka.stream, spray.caching, maxmind, prismic, kamon.core, kamon.statsd, pushy, java8compat, semver), TwirlKeys.templateImports ++= Seq( @@ -321,7 +322,7 @@ object ApplicationBuild extends Build { ) lazy val socket = project("socket", Seq(common, hub, memo)).settings( - libraryDependencies ++= provided(play.api) + libraryDependencies ++= provided(play.api, akka.stream) ) lazy val hub = project("hub", Seq(common, chess)).settings( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f9bb25f3f9..528c8b71d2 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -56,6 +56,7 @@ object Dependencies { val version = "2.4.2" val actor = "com.typesafe.akka" %% "akka-actor" % version val slf4j = "com.typesafe.akka" %% "akka-slf4j" % version + val stream = "com.typesafe.akka" %% "akka-stream" % version } object kamon { val version = "0.5.2" From aa63948060fe4ae50617e5450e5fab304bd9aa8a Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 1 Apr 2016 11:44:20 +0700 Subject: [PATCH 04/26] rewrite LilaSocket --- app/controllers/LilaSocket.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/LilaSocket.scala b/app/controllers/LilaSocket.scala index 10fe5b20d4..51941d8144 100644 --- a/app/controllers/LilaSocket.scala +++ b/app/controllers/LilaSocket.scala @@ -16,8 +16,8 @@ trait LilaSocket { self: LilaController => private val logger = lila.log("ratelimit") - def rateLimitedSocket[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket[A, A] = - WebSocket[A, A] { req => + def rateLimitedSocket[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket = + WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap { ctx => val ip = HTTPRequest lastRemoteAddress req def userInfo = { @@ -28,9 +28,8 @@ trait LilaSocket { self: LilaController => // logger.debug(s"socket:$name socket connect $ip $userInfo") f(ctx).map { resultOrSocket => resultOrSocket.right.map { - case (readIn, writeOut) => (e, i) => { - writeOut |>> i - e &> Enumeratee.mapInputM { in => + case (readIn, writeOut) => { + val limitedIn = Enumeratee.mapInputM[A] { in => consumer(ip).map { credit => if (credit >= 0) in else { @@ -38,7 +37,8 @@ trait LilaSocket { self: LilaController => Input.EOF } } - } |>> readIn + } &> readIn + (limitedIn, writeOut) } } } From d0656c8b64da02fd4679d0a422788a2ef20f809a Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 2 Apr 2016 17:29:10 +0700 Subject: [PATCH 05/26] play 2.5 WIP --- app/controllers/Tv.scala | 9 +- modules/common/src/main/PackageObject.scala | 2 + modules/round/src/main/TvBroadcast.scala | 20 +++-- project/Build.scala | 97 +++++++++++---------- project/Dependencies.scala | 2 + 5 files changed, 70 insertions(+), 60 deletions(-) diff --git a/app/controllers/Tv.scala b/app/controllers/Tv.scala index 4cdf6e03a5..4b99160209 100644 --- a/app/controllers/Tv.scala +++ b/app/controllers/Tv.scala @@ -79,10 +79,11 @@ object Tv extends LilaController { import akka.pattern.ask import lila.round.TvBroadcast import play.api.libs.EventSource - implicit val encoder = play.api.libs.Comet.CometMessage.jsonMessages - Env.round.tvBroadcast ? TvBroadcast.GetEnumerator mapTo - manifest[TvBroadcast.EnumeratorType] map { enum => - Ok.chunked(enum &> EventSource()).as("text/event-stream") + import akka.stream.scaladsl.Source + Env.round.tvBroadcast ? TvBroadcast.GetPublisher mapTo + manifest[TvBroadcast.PublisherType] map { publisher => + val source = Source fromPublisher publisher + Ok.chunked(source via EventSource.flow).as("text/event-stream") } } diff --git a/modules/common/src/main/PackageObject.scala b/modules/common/src/main/PackageObject.scala index 204ef2a21e..aa34183892 100644 --- a/modules/common/src/main/PackageObject.scala +++ b/modules/common/src/main/PackageObject.scala @@ -97,6 +97,8 @@ trait WithPlay { self: PackageObject => implicit def execontext = play.api.libs.concurrent.Execution.defaultContext + implicit def materializer(implicit system: akka.actor.ActorSystem) = akka.stream.ActorMaterializer() + implicit val LilaFutureMonad = new Monad[Fu] { override def map[A, B](fa: Fu[A])(f: A => B) = fa map f def point[A](a: => A) = fuccess(a) diff --git a/modules/round/src/main/TvBroadcast.scala b/modules/round/src/main/TvBroadcast.scala index 3d1baf435f..c194adbce3 100644 --- a/modules/round/src/main/TvBroadcast.scala +++ b/modules/round/src/main/TvBroadcast.scala @@ -1,28 +1,32 @@ package lila.round import akka.actor._ +import akka.stream._ +import akka.stream.scaladsl._ import lila.hub.actorApi.game.ChangeFeatured import lila.hub.actorApi.round.MoveEvent import lila.socket.Socket.makeMessage -import play.api.libs.iteratee._ import play.api.libs.json._ private final class TvBroadcast extends Actor { - private val (enumerator, channel) = Concurrent.broadcast[JsValue] + implicit val mat = materializer(context.system) + + val (actorRef, publisher) = Source.actorRef(20, OverflowStrategy.dropHead) + .toMat(Sink asPublisher false)(Keep.both).run() private var featuredId = none[String] def receive = { - case TvBroadcast.GetEnumerator => sender ! enumerator + case TvBroadcast.GetPublisher => sender ! publisher case ChangeFeatured(id, msg) => featuredId = id.some - channel push msg + actorRef ! msg case move: MoveEvent if Some(move.gameId) == featuredId => - channel push makeMessage("fen", Json.obj( + actorRef ! makeMessage("fen", Json.obj( "fen" -> move.fen, "lm" -> move.move )) @@ -31,7 +35,7 @@ private final class TvBroadcast extends Actor { object TvBroadcast { - type EnumeratorType = Enumerator[JsValue] - - case object GetEnumerator + import org.reactivestreams.Publisher + type PublisherType = Publisher[JsValue] + case object GetPublisher } diff --git a/project/Build.scala b/project/Build.scala index 4a476bc7b5..afdd121ac3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -35,9 +35,10 @@ object ApplicationBuild extends Build { // offline := true, libraryDependencies ++= Seq( scalaz, scalalib, hasher, config, apache, - jgit, findbugs, RM, PRM, akka.actor, akka.slf4j, + jgit, findbugs, RM, akka.actor, akka.slf4j, akka.stream, spray.caching, maxmind, prismic, - kamon.core, kamon.statsd, pushy, java8compat, semver), + kamon.core, kamon.statsd, pushy, java8compat, semver, + jzlib), TwirlKeys.templateImports ++= Seq( "lila.game.{ Game, Player, Pov }", "lila.tournament.Tournament", @@ -76,23 +77,23 @@ object ApplicationBuild extends Build { lazy val puzzle = project("puzzle", Seq( common, memo, hub, db, user, rating)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val quote = project("quote", Seq()) lazy val opening = project("opening", Seq( common, memo, hub, db, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val video = project("video", Seq( common, memo, hub, db, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val coordinate = project("coordinate", Seq(common, db)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val worldMap = project("worldMap", Seq(common, hub, memo, rating)).settings( @@ -100,21 +101,21 @@ object ApplicationBuild extends Build { ) lazy val qa = project("qa", Seq(common, db, memo, user, security)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val blog = project("blog", Seq(common, memo, user, message)).settings( - libraryDependencies ++= provided(play.api, RM, PRM, prismic) + libraryDependencies ++= provided(play.api, RM, prismic) ) lazy val donation = project("donation", Seq( common, db, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val evaluation = project("evaluation", Seq( common, hub, db, user, game, analyse)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) // lazy val simulation = project("simulation", Seq( @@ -127,19 +128,19 @@ object ApplicationBuild extends Build { ) lazy val rating = project("rating", Seq(common, db, chess)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val perfStat = project("perfStat", Seq(common, db, chess, user, game, rating)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val history = project("history", Seq(common, db, memo, game, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val db = project("db", Seq(common)).settings( - libraryDependencies ++= provided(play.test, play.api, RM, PRM) + libraryDependencies ++= provided(play.test, play.api, RM) ) lazy val memo = project("memo", Seq(common, db)).settings( @@ -151,138 +152,138 @@ object ApplicationBuild extends Build { ) lazy val chat = project("chat", Seq(common, db, user, security, i18n)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val timeline = project("timeline", Seq(common, db, game, user, hub, security, relation)).settings( libraryDependencies ++= provided( - play.api, play.test, RM, PRM) + play.api, play.test, RM) ) lazy val mod = project("mod", Seq(common, db, user, hub, security, game, analyse, evaluation, report)).settings( libraryDependencies ++= provided( - play.api, play.test, RM, PRM) + play.api, play.test, RM) ) lazy val user = project("user", Seq(common, memo, db, hub, chess, rating)).settings( libraryDependencies ++= provided( - play.api, play.test, RM, PRM, hasher) + play.api, play.test, RM, hasher) ) lazy val game = project("game", Seq(common, memo, db, hub, user, chess, chat)).settings( libraryDependencies ++= provided( - play.api, RM, PRM) + play.api, RM) ) lazy val gameSearch = project("gameSearch", Seq(common, hub, chess, search, game)).settings( libraryDependencies ++= provided( - play.api, RM, PRM) + play.api, RM) ) lazy val tv = project("tv", Seq(common, db, hub, socket, game, user, chess)).settings( - libraryDependencies ++= provided(play.api, RM, PRM, hasher) + libraryDependencies ++= provided(play.api, RM, hasher) ) lazy val analyse = project("analyse", Seq(common, hub, chess, game, user)).settings( libraryDependencies ++= provided( - play.api, RM, PRM, spray.caching) + play.api, RM, spray.caching) ) lazy val round = project("round", Seq( common, db, memo, hub, socket, chess, game, user, i18n, fishnet, pref, chat, history, playban)).settings( - libraryDependencies ++= provided(play.api, RM, PRM, hasher, kamon.core) + libraryDependencies ++= provided(play.api, RM, hasher, kamon.core) ) lazy val lobby = project("lobby", Seq( common, db, memo, hub, socket, chess, game, user, round, timeline, relation, playban, security)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val setup = project("setup", Seq( common, db, memo, hub, socket, chess, game, user, lobby, pref, relation)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val importer = project("importer", Seq(common, chess, game, round)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val insight = project("insight", Seq(common, chess, game, user, analyse, relation, pref, socket, round, security) ).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val tournament = project("tournament", Seq( common, hub, socket, chess, game, round, security, chat, memo, quote)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val simul = project("simul", Seq( common, hub, socket, chess, game, round, chat, memo, quote)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val fishnet = project("fishnet", Seq(common, chess, game, analyse, db)).settings( - libraryDependencies ++= provided(play.api, RM, PRM, semver) + libraryDependencies ++= provided(play.api, RM, semver) ) lazy val security = project("security", Seq(common, hub, db, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM, maxmind, hasher) + libraryDependencies ++= provided(play.api, RM, maxmind, hasher) ) lazy val shutup = project("shutup", Seq(common, db, hub, game, relation)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val challenge = project("challenge", Seq(common, db, hub, setup, game)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val playban = project("playban", Seq(common, db, game)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val push = project("push", Seq(common, db, user, game, challenge)).settings( - libraryDependencies ++= provided(play.api, RM, PRM, pushy) + libraryDependencies ++= provided(play.api, RM, pushy) ) lazy val slack = project("slack", Seq(common, hub, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val relation = project("relation", Seq(common, db, memo, hub, user, game, pref)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val pref = project("pref", Seq(common, db, user)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val message = project("message", Seq(common, db, user, hub, relation, security)).settings( libraryDependencies ++= provided( - play.api, RM, PRM, spray.caching) + play.api, RM, spray.caching) ) lazy val forum = project("forum", Seq(common, db, user, security, hub, mod)).settings( libraryDependencies ++= provided( - play.api, RM, PRM, spray.caching) + play.api, RM, spray.caching) ) lazy val forumSearch = project("forumSearch", Seq(common, hub, forum, search)).settings( libraryDependencies ++= provided( - play.api, RM, PRM) + play.api, RM) ) lazy val team = project("team", Seq(common, memo, db, user, forum, security, hub)).settings( libraryDependencies ++= provided( - play.api, RM, PRM) + play.api, RM) ) lazy val teamSearch = project("teamSearch", Seq(common, hub, team, search)).settings( libraryDependencies ++= provided( - play.api, RM, PRM) + play.api, RM) ) lazy val i18n = project("i18n", Seq(common, db, user, hub)).settings( @@ -292,26 +293,26 @@ object ApplicationBuild extends Build { (sourceManaged in Compile).value / "messages" ) }.taskValue, - libraryDependencies ++= provided(play.api, RM, PRM, jgit) + libraryDependencies ++= provided(play.api, RM, jgit) ) lazy val bookmark = project("bookmark", Seq(common, memo, db, hub, user, game)).settings( libraryDependencies ++= provided( - play.api, play.test, RM, PRM) + play.api, play.test, RM) ) lazy val wiki = project("wiki", Seq(common, db)).settings( libraryDependencies ++= provided( - play.api, RM, PRM, jgit, guava) + play.api, RM, jgit, guava) ) lazy val report = project("report", Seq(common, db, user)).settings( libraryDependencies ++= provided( - play.api, RM, PRM) + play.api, RM) ) lazy val explorer = project("explorer", Seq(common, db, game)).settings( - libraryDependencies ++= provided(play.api, RM, PRM) + libraryDependencies ++= provided(play.api, RM) ) lazy val notification = project("notification", Seq(common, user, hub)).settings( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index a8ca32ef03..5deedcec64 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -41,6 +41,7 @@ object Dependencies { val pushy = "com.relayrides" % "pushy" % "0.4.3" val java8compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0" val semver = "com.gilt" %% "gfc-semver" % "0.0.2-9-g11173e1" + val jzlib = "com.jcraft" % "jzlib" % "1.1.3" object play { val version = "2.5.1" @@ -56,6 +57,7 @@ object Dependencies { val version = "2.4.2" val actor = "com.typesafe.akka" %% "akka-actor" % version val slf4j = "com.typesafe.akka" %% "akka-slf4j" % version + val stream = "com.typesafe.akka" %% "akka-stream" % version } object kamon { val version = "0.5.2" From 71a4402a1f6c061b2b5663b0a2f7c8f9a41f2b4e Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 17:37:07 +0700 Subject: [PATCH 06/26] play 2.5 migration WIP --- app/Global.scala | 45 +---------------- app/Lila.scala | 76 +++++++++++++++++++++++++++++ app/controllers/Prismic.scala | 10 ++-- bin/build-deps.sh | 44 ++++++++--------- conf/base.conf | 2 + modules/blog/src/main/BlogApi.scala | 10 ++-- project/Dependencies.scala | 2 +- 7 files changed, 117 insertions(+), 72 deletions(-) create mode 100644 app/Lila.scala diff --git a/app/Global.scala b/app/Global.scala index 86a6c23ac8..80c94828c8 100644 --- a/app/Global.scala +++ b/app/Global.scala @@ -1,51 +1,10 @@ package lila.app -import lila.common.HTTPRequest -import play.api.mvc._ -import play.api.mvc.Results._ -import play.api.{ Application, GlobalSettings, Mode } +import play.api.{ Application, GlobalSettings } object Global extends GlobalSettings { override def onStart(app: Application) { - kamon.Kamon.start() - lila.app.Env.current + lila.app.Env.current // preload modules } - - override def onStop(app: Application) { - kamon.Kamon.shutdown() - } - - override def onRouteRequest(req: RequestHeader): Option[Handler] = { - lila.mon.http.request.all() - Env.i18n.requestHandler(req) orElse super.onRouteRequest(req) - } - - private def niceError(req: RequestHeader): Boolean = - req.method == "GET" && - HTTPRequest.isSynchronousHttp(req) && - !HTTPRequest.hasFileExtension(req) - - override def onHandlerNotFound(req: RequestHeader) = - if (niceError(req)) controllers.Main.notFound(req) - else fuccess(NotFound("404 - Resource not found")) - - override def onBadRequest(req: RequestHeader, error: String) = - if (error startsWith "Illegal character in path") fuccess(Redirect("/")) - else if (error startsWith "Cannot parse parameter") onHandlerNotFound(req) - else if (niceError(req)) { - lila.mon.http.response.code400() - controllers.Lobby.handleStatus(req, Results.BadRequest) - } - else fuccess(BadRequest(error)) - - override def onError(req: RequestHeader, ex: Throwable) = - if (niceError(req)) { - if (lila.common.PlayApp.isProd) { - lila.mon.http.response.code500() - fuccess(InternalServerError(views.html.base.errorPage(ex)(lila.api.Context(req)))) - } - else super.onError(req, ex) - } - else fuccess(InternalServerError(ex.getMessage)) } diff --git a/app/Lila.scala b/app/Lila.scala new file mode 100644 index 0000000000..eb0045d96a --- /dev/null +++ b/app/Lila.scala @@ -0,0 +1,76 @@ +package lila.app + +import com.google.inject.AbstractModule +import com.google.inject.name.Names +import javax.inject._ +import lila.common.HTTPRequest +import play.api._ +import play.api.http._ +import play.api.inject.ApplicationLifecycle +import play.api.mvc._ +import play.api.mvc.RequestHeader +import play.api.mvc.Results._ +import play.api.routing.Router +import scala.concurrent._ + +@Singleton +final class LilaGlobal @Inject() (lifecycle: ApplicationLifecycle) { + + play.api.Logger("boot").info("========================= Lifecycle bindings") + + lifecycle.addStopHook { () => + fuccess { + kamon.Kamon.shutdown() + } + } +} + +class LilaHttpRequestHandler @Inject() (errorHandler: HttpErrorHandler, + configuration: HttpConfiguration, filters: HttpFilters, + router: Router) extends DefaultHttpRequestHandler(router, errorHandler, configuration, filters) { + + override def routeRequest(req: RequestHeader) = { + lila.mon.http.request.all() + Env.i18n.requestHandler(req) orElse super.routeRequest(req) + } +} + +class ErrorHandler @Inject() ( + env: Environment, + config: Configuration, + sourceMapper: OptionalSourceMapper, + router: Provider[Router]) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) { + + private def niceError(req: RequestHeader): Boolean = + req.method == "GET" && + HTTPRequest.isSynchronousHttp(req) && + !HTTPRequest.hasFileExtension(req) + + def onHandlerNotFound(req: RequestHeader) = + if (niceError(req)) controllers.Main.notFound(req) + else fuccess(NotFound("404 - Resource not found")) + + override def onBadRequest(req: RequestHeader, error: String) = + if (error startsWith "Illegal character in path") fuccess(Redirect("/")) + else if (error startsWith "Cannot parse parameter") onHandlerNotFound(req) + else if (niceError(req)) { + lila.mon.http.response.code400() + controllers.Lobby.handleStatus(req, Results.BadRequest) + } + else fuccess(BadRequest(error)) + + override def onProdServerError(req: RequestHeader, ex: UsefulException): Future[Result] = { + lila.mon.http.response.code500() + fuccess(InternalServerError(views.html.base.errorPage(ex)(lila.api.Context(req)))) + } +} + +final class LilaModule extends AbstractModule { + + kamon.Kamon.start() + + def configure() = { + bind(classOf[LilaGlobal]).asEagerSingleton + // bind[LilaGlobal] toSelf eagerly() + } +} diff --git a/app/controllers/Prismic.scala b/app/controllers/Prismic.scala index b256c06854..9c18532d96 100644 --- a/app/controllers/Prismic.scala +++ b/app/controllers/Prismic.scala @@ -17,8 +17,12 @@ object Prismic { case _ => logger info message } + import play.api.libs.ws._ + import play.api.Play.current + private val httpClient: WSClient = WS.client + private val fetchPrismicApi = AsyncCache.single[PrismicApi]( - f = PrismicApi.get(Env.api.PrismicApiUrl, logger = prismicLogger), + f = PrismicApi.get(httpClient)(Env.api.PrismicApiUrl, logger = prismicLogger), timeToLive = 1 minute) def prismicApi = fetchPrismicApi(true) @@ -33,7 +37,7 @@ object Prismic { api.forms("everything") .query(s"""[[:d = at(document.id, "$id")]]""") .ref(api.master.ref) - .submit() map { + .submit(httpClient) map { _.results.headOption } } @@ -52,7 +56,7 @@ object Prismic { api.forms("variant") .query(s"""[[:d = at(my.variant.key, "${variant.key}")]]""") .ref(api.master.ref) - .submit() map { + .submit(httpClient) map { _.results.headOption map (_ -> makeLinkResolver(api)) } } diff --git a/bin/build-deps.sh b/bin/build-deps.sh index b00d5e44d8..75ce7de19e 100755 --- a/bin/build-deps.sh +++ b/bin/build-deps.sh @@ -5,35 +5,35 @@ dir=$(mktemp -d) echo "Building in $dir" cd "$dir" -git clone https://github.com/gilt/gfc-semver -cd gfc-semver -sbt publish-local -cd .. +# git clone https://github.com/gilt/gfc-semver +# cd gfc-semver +# sbt publish-local +# cd .. -git clone https://github.com/ornicar/ReactiveMongo --branch lichess -cd ReactiveMongo -sbt publish-local -cd .. +# git clone https://github.com/ornicar/ReactiveMongo --branch lichess +# cd ReactiveMongo +# sbt publish-local +# cd .. -git clone https://github.com/ornicar/scalalib -cd scalalib -sbt publish-local -cd .. +# git clone https://github.com/ornicar/scalalib +# cd scalalib +# sbt publish-local +# cd .. git clone https://github.com/ornicar/scala-kit --branch lichess-fork cd scala-kit -git checkout b019b3a2522d3f1697c39ec0c79e88c18ea49a91 -sbt -Dversion=1.2.11-THIB publish-local +git checkout 75fb61c8306a7a3db4c5dae6f110972130e7c6bc +sbt -Dversion=1.2.11-THIB2 publish-local cd .. -git clone https://github.com/ornicar/maxmind-geoip2-scala --branch customBuild -cd maxmind-geoip2-scala -sbt publish-local -cd .. +# git clone https://github.com/ornicar/maxmind-geoip2-scala --branch customBuild +# cd maxmind-geoip2-scala +# sbt publish-local +# cd .. -git clone https://github.com/Nycto/Hasher -cd Hasher -sbt publish-local -cd .. +# git clone https://github.com/Nycto/Hasher +# cd Hasher +# sbt publish-local +# cd .. rm -rf "$dir" diff --git a/conf/base.conf b/conf/base.conf index c94cd8a0e1..50916c757f 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -14,6 +14,8 @@ net { } forcedev = false play { + modules.enabled += "lila.app.LilaModule" + http.requestHandler = "lila.app.LilaHttpRequestHandler" server { netty { # The maximum length of the initial line. This effectively restricts the maximum length of a URL that the server will diff --git a/modules/blog/src/main/BlogApi.scala b/modules/blog/src/main/BlogApi.scala index 28bca8151e..0e20ddc3c0 100644 --- a/modules/blog/src/main/BlogApi.scala +++ b/modules/blog/src/main/BlogApi.scala @@ -7,15 +7,19 @@ import scala.concurrent.duration._ final class BlogApi(prismicUrl: String, collection: String) { + import play.api.libs.ws._ + import play.api.Play.current + private val httpClient: WSClient = WS.client + def recent(api: Api, ref: Option[String], nb: Int): Fu[Option[Response]] = api.forms(collection).ref(resolveRef(api)(ref) | api.master.ref) .orderings(s"[my.$collection.date desc]") - .pageSize(nb).page(1).submit().fold(_ => none, some _) + .pageSize(nb).page(1).submit(httpClient).fold(_ => none, some _) def one(api: Api, ref: Option[String], id: String) = api.forms(collection) .query(s"""[[:d = at(document.id, "$id")]]""") - .ref(resolveRef(api)(ref) | api.master.ref).submit() map (_.results.headOption) + .ref(resolveRef(api)(ref) | api.master.ref).submit(httpClient) map (_.results.headOption) // -- Build a Prismic context def context(refName: Option[String])(implicit linkResolver: (Api, Option[String]) => DocumentLinkResolver) = @@ -40,7 +44,7 @@ final class BlogApi(prismicUrl: String, collection: String) { } private val fetchPrismicApi = AsyncCache.single[Api]( - f = Api.get(prismicUrl, cache = cache, logger = prismicLogger), + f = Api.get(httpClient)(prismicUrl, cache = cache, logger = prismicLogger), timeToLive = 10 seconds) def prismicApi = fetchPrismicApi(true) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 721e973a2f..814b96640d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -36,7 +36,7 @@ object Dependencies { val jodaTime = "joda-time" % "joda-time" % "2.9.2" val RM = "org.reactivemongo" % "reactivemongo_2.11" % "0.11.9.1-LILA" val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.2.3-THIB" - val prismic = "io.prismic" %% "scala-kit" % "1.2.11-THIB" + val prismic = "io.prismic" %% "scala-kit" % "1.2.11-THIB2" val pushy = "com.relayrides" % "pushy" % "0.4.3" val java8compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0" val semver = "com.gilt" %% "gfc-semver" % "0.0.2-9-g11173e1" From 9b1d82bf8429222d313698cbe0b4ccb2ef649a7b Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 19:16:10 +0700 Subject: [PATCH 07/26] Global is deprecated, rewrite event handling --- app/{Lila.scala => LilaPlayStuff.scala} | 30 ++++++++++++------------- app/controllers/Lobby.scala | 1 + app/views/base/errorPage.scala.html | 29 ++++++++++++++++-------- conf/base.conf | 1 + 4 files changed, 37 insertions(+), 24 deletions(-) rename app/{Lila.scala => LilaPlayStuff.scala} (85%) diff --git a/app/Lila.scala b/app/LilaPlayStuff.scala similarity index 85% rename from app/Lila.scala rename to app/LilaPlayStuff.scala index eb0045d96a..8d96cca9c8 100644 --- a/app/Lila.scala +++ b/app/LilaPlayStuff.scala @@ -13,18 +13,6 @@ import play.api.mvc.Results._ import play.api.routing.Router import scala.concurrent._ -@Singleton -final class LilaGlobal @Inject() (lifecycle: ApplicationLifecycle) { - - play.api.Logger("boot").info("========================= Lifecycle bindings") - - lifecycle.addStopHook { () => - fuccess { - kamon.Kamon.shutdown() - } - } -} - class LilaHttpRequestHandler @Inject() (errorHandler: HttpErrorHandler, configuration: HttpConfiguration, filters: HttpFilters, router: Router) extends DefaultHttpRequestHandler(router, errorHandler, configuration, filters) { @@ -35,7 +23,7 @@ class LilaHttpRequestHandler @Inject() (errorHandler: HttpErrorHandler, } } -class ErrorHandler @Inject() ( +class LilaHttpErrorHandler @Inject() ( env: Environment, config: Configuration, sourceMapper: OptionalSourceMapper, @@ -65,12 +53,24 @@ class ErrorHandler @Inject() ( } } +@Singleton +final class LilaLifecycle @Inject() (lifecycle: ApplicationLifecycle) { + + play.api.Logger("boot").info("Lifecycle bindings") + + lifecycle.addStopHook { () => + play.api.Logger("play").info("LilaLifecycle shutdown") + kamon.Kamon.shutdown() + funit + } +} + final class LilaModule extends AbstractModule { + play.api.Logger("boot").info("Kamon start") kamon.Kamon.start() def configure() = { - bind(classOf[LilaGlobal]).asEagerSingleton - // bind[LilaGlobal] toSelf eagerly() + bind(classOf[LilaLifecycle]).asEagerSingleton } } diff --git a/app/controllers/Lobby.scala b/app/controllers/Lobby.scala index 24b2b1d4d3..f74bb0c069 100644 --- a/app/controllers/Lobby.scala +++ b/app/controllers/Lobby.scala @@ -11,6 +11,7 @@ import views._ object Lobby extends LilaController { def home = Open { implicit ctx => + sys error "oooooooopsies" negotiate( html = renderHome(Results.Ok).map(NoCache), api = _ => fuccess { diff --git a/app/views/base/errorPage.scala.html b/app/views/base/errorPage.scala.html index 348fa41b6a..335a63a41a 100644 --- a/app/views/base/errorPage.scala.html +++ b/app/views/base/errorPage.scala.html @@ -1,16 +1,27 @@ @(ex: Throwable)(implicit ctx: Context) @site.layout( -title = "Internal server error") { +title = "Internal server error", +moreCss = cssTag("notFound.css")) {
-

Something went wrong on this page.

-
-
-

If the problem persists, please report it in the forum.

-

Or send me an email at lichess.contact@gmail.com

-
-
- @ex.getMessage +
+

500

+ Server error +

Sorry! We're working on it! Maybe try this mini-game?

+
+
+ +

+ ChessPursuit + courtesy of + Saturnyn +

+
} diff --git a/conf/base.conf b/conf/base.conf index 50916c757f..4a7e21b195 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -16,6 +16,7 @@ forcedev = false play { modules.enabled += "lila.app.LilaModule" http.requestHandler = "lila.app.LilaHttpRequestHandler" + http.errorHandler = "lila.app.LilaHttpErrorHandler" server { netty { # The maximum length of the initial line. This effectively restricts the maximum length of a URL that the server will From 34fb45c258b7fdf054e871877df1c59afc3be7f4 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 21:09:14 +0700 Subject: [PATCH 08/26] remove debug --- app/controllers/Lobby.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/Lobby.scala b/app/controllers/Lobby.scala index f74bb0c069..24b2b1d4d3 100644 --- a/app/controllers/Lobby.scala +++ b/app/controllers/Lobby.scala @@ -11,7 +11,6 @@ import views._ object Lobby extends LilaController { def home = Open { implicit ctx => - sys error "oooooooopsies" negotiate( html = renderHome(Results.Ok).map(NoCache), api = _ => fuccess { From 84b790dbf89780a5ce8675d455960c3dd1d3f608 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 22:42:37 +0700 Subject: [PATCH 09/26] explicit config --- conf/base.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conf/base.conf b/conf/base.conf index 694e87963d..86e2c255ae 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -27,6 +27,10 @@ play { # The maximum length of the HTTP headers. The most common effect of this is a restriction in cookie length, including # number of cookies and size of cookie values. maxHeaderSize = 4096 # 8192 + + # "native is faster but only available on linux + # "native" is enabled in production + transport = "jdk" } } i18n { @@ -41,6 +45,7 @@ play { ws { useragent = ${net.base_url} compressionEnabled = true + followRedirects = true timeout { connection = 5 seconds idle = 5 minutes From c8a0a1845294283dcc9adc4336aa5f347addd018 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 22:42:44 +0700 Subject: [PATCH 10/26] fix forum DB queries --- modules/forum/src/main/PostApi.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/forum/src/main/PostApi.scala b/modules/forum/src/main/PostApi.scala index 97fe145bfa..0cb08bb887 100644 --- a/modules/forum/src/main/PostApi.scala +++ b/modules/forum/src/main/PostApi.scala @@ -89,12 +89,12 @@ final class PostApi( def get(postId: String): Fu[Option[(Topic, Post)]] = for { post ← optionT(env.postColl.byId[Post](postId)) - topic ← optionT(env.postColl.byId[Topic](post.topicId)) + topic ← optionT(env.topicColl.byId[Topic](post.topicId)) } yield topic -> post def views(posts: List[Post]): Fu[List[PostView]] = for { - topics ← env.postColl.byIds[Topic](posts.map(_.topicId).distinct) - categs ← env.postColl.byIds[Categ](topics.map(_.categId).distinct) + topics ← env.topicColl.byIds[Topic](posts.map(_.topicId).distinct) + categs ← env.categColl.byIds[Categ](topics.map(_.categId).distinct) } yield posts map { post => for { topic ← topics find (_.id == post.topicId) From 1d5cc0f7ab5f221d65e1877f281ff28716cb1b31 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 22:53:48 +0700 Subject: [PATCH 11/26] fix user.sha512 flag mapping --- modules/user/src/main/UserRepo.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index 1ded27bc3c..304c479ebc 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -195,8 +195,8 @@ object UserRepo { def authenticateByEmail(email: String, password: String): Fu[Option[User]] = checkPasswordByEmail(email, password) flatMap { _ ?? byEmail(email) } - private case class AuthData(password: String, salt: String, enabled: Boolean, sha512: Boolean) { - def compare(p: String) = password == sha512.fold(hash512(p, salt), hash(p, salt)) + private case class AuthData(password: String, salt: String, enabled: Boolean, sha512: Option[Boolean]) { + def compare(p: String) = password == (~sha512).fold(hash512(p, salt), hash(p, salt)) } private implicit val AuthDataBSONHandler = Macros.handler[AuthData] From 301cc43e59e37b6f625e7047ff9e76528e51b1e9 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 23:06:08 +0700 Subject: [PATCH 12/26] tweak WS config --- conf/base.conf | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/conf/base.conf b/conf/base.conf index 86e2c255ae..5c2fa81365 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -47,10 +47,22 @@ play { compressionEnabled = true followRedirects = true timeout { - connection = 5 seconds - idle = 5 minutes - request = 5 minutes + connection = 10 seconds + idle = 30 seconds + request = 1 minute } + + # Configuration specific to the Ahc implementation of the WS client + ahc { + keepAlive = true + maxNumberOfRedirects = 3 + maxRequestRetry = 3 + # If non null, the maximum time that a connection should live for in the pool. + maxConnectionLifetime = null + # If non null, the time after which a connection that has been idle in the pool should be closed. + idleConnectionInPoolTimeout = 1 minute + } + } } crypto { secret="CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^" From 421c93f09e0e7c8742af7dec7d53d5b65ab4b0a5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 23:34:49 +0700 Subject: [PATCH 13/26] tweak UserRepo --- modules/user/src/main/UserRepo.scala | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index 304c479ebc..e2c5dc3f73 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -68,7 +68,7 @@ object UserRepo { $inIds(ids) ++ goodLadSelectBson, $doc("_id" -> true)) .sort($doc(s"perfs.standard.gl.r" -> -1)) - .cursor[BSONDocument](ReadPreference.secondaryPreferred) + .cursor[Bdoc](ReadPreference.secondaryPreferred) .gather[List](nb).map { _.flatMap { _.getAs[String]("_id") } } @@ -86,9 +86,9 @@ object UserRepo { coll.find( $doc("_id".$in(u1, u2)), $doc(s"${F.count}.game" -> true) - ).cursor[BSONDocument]().gather[List]() map { docs => + ).cursor[Bdoc]().gather[List]() map { docs => docs.sortBy { - _.getAs[BSONDocument](F.count).flatMap(_.getAs[BSONNumberLike]("game")).??(_.toInt) + _.getAs[Bdoc](F.count).flatMap(_.getAs[BSONNumberLike]("game")).??(_.toInt) }.map(_.getAs[String]("_id")).flatten match { case List(u1, u2) => (u1, u2).some case _ => none @@ -101,7 +101,7 @@ object UserRepo { case (u1, u2) => coll.find( $doc("_id".$in(u1, u2)), $doc("_id" -> true) - ).sort($doc(F.colorIt -> 1)).uno[BSONDocument].map { + ).sort($doc(F.colorIt -> 1)).uno[Bdoc].map { _.fold(scala.util.Random.nextBoolean) { doc => doc.getAs[String]("_id") contains u1 } @@ -207,7 +207,7 @@ object UserRepo { def checkPasswordByEmail(email: String, password: String): Fu[Boolean] = checkPassword($doc(F.email -> email), password) - private def checkPassword(select: BSONDocument, password: String): Fu[Boolean] = + private def checkPassword(select: Bdoc, password: String): Fu[Boolean] = coll.uno[AuthData](select) map { _ ?? (data => data.enabled && data.compare(password)) } @@ -233,9 +233,9 @@ object UserRepo { import java.util.regex.Matcher.quoteReplacement val escaped = """^([\w-]*).*$""".r.replaceAllIn(normalize(username), m => quoteReplacement(m group 1)) val regex = "^" + escaped + ".*$" - coll.find($doc("_id" -> BSONRegex(regex, "")), $doc(F.username -> true)) + coll.find($id($regex(regex, "")), $doc(F.username -> true)) .sort($sort desc "_id") - .cursor[BSONDocument]().gather[List](max) + .cursor[Bdoc]().gather[List](max) .map { _ flatMap { _.getAs[String](F.username) } } @@ -243,7 +243,7 @@ object UserRepo { def toggleEngine(id: ID): Funit = coll.fetchUpdate[User]($id(id)) { u => - $set("engine" -> BSONBoolean(!u.engine)) + $set("engine" -> !u.engine) } def setEngine(id: ID, v: Boolean): Funit = coll.updateField($id(id), "engine", v).void @@ -291,8 +291,8 @@ object UserRepo { def perfOf(id: ID, perfType: PerfType): Fu[Option[Perf]] = coll.find( $doc("_id" -> id), $doc(s"${F.perfs}.${perfType.key}" -> true) - ).uno[BSONDocument].map { - _.flatMap(_.getAs[BSONDocument](F.perfs)).flatMap(_.getAs[Perf](perfType.key)) + ).uno[Bdoc].map { + _.flatMap(_.getAs[Bdoc](F.perfs)).flatMap(_.getAs[Perf](perfType.key)) } def setSeenAt(id: ID) { @@ -305,7 +305,7 @@ object UserRepo { "seenAt" -> $doc("$gt" -> since), "count.game" -> $doc("$gt" -> 9), "kid" -> $doc("$ne" -> true) - ), $doc("_id" -> true)).cursor[BSONDocument]() + ), $doc("_id" -> true)).cursor[Bdoc]() def setLang(id: ID, lang: String) = coll.updateField($id(id), "lang", lang).void From 91ffacb5f1eaadba930e9189a47b8d422e263a20 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Apr 2016 23:37:17 +0700 Subject: [PATCH 14/26] fix build-deps --- bin/build-deps.sh | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/bin/build-deps.sh b/bin/build-deps.sh index 75ce7de19e..19135ad3a2 100755 --- a/bin/build-deps.sh +++ b/bin/build-deps.sh @@ -5,20 +5,20 @@ dir=$(mktemp -d) echo "Building in $dir" cd "$dir" -# git clone https://github.com/gilt/gfc-semver -# cd gfc-semver -# sbt publish-local -# cd .. +git clone https://github.com/gilt/gfc-semver +cd gfc-semver +sbt publish-local +cd .. -# git clone https://github.com/ornicar/ReactiveMongo --branch lichess -# cd ReactiveMongo -# sbt publish-local -# cd .. +git clone https://github.com/ornicar/ReactiveMongo --branch lichess +cd ReactiveMongo +sbt publish-local +cd .. -# git clone https://github.com/ornicar/scalalib -# cd scalalib -# sbt publish-local -# cd .. +git clone https://github.com/ornicar/scalalib +cd scalalib +sbt publish-local +cd .. git clone https://github.com/ornicar/scala-kit --branch lichess-fork cd scala-kit @@ -26,14 +26,14 @@ git checkout 75fb61c8306a7a3db4c5dae6f110972130e7c6bc sbt -Dversion=1.2.11-THIB2 publish-local cd .. -# git clone https://github.com/ornicar/maxmind-geoip2-scala --branch customBuild -# cd maxmind-geoip2-scala -# sbt publish-local -# cd .. +git clone https://github.com/ornicar/maxmind-geoip2-scala --branch customBuild +cd maxmind-geoip2-scala +sbt publish-local +cd .. -# git clone https://github.com/Nycto/Hasher -# cd Hasher -# sbt publish-local -# cd .. +git clone https://github.com/Nycto/Hasher +cd Hasher +sbt publish-local +cd .. rm -rf "$dir" From 49f8fc9fe8304efff200ad13fdb5c0f053c05d84 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 00:03:51 +0700 Subject: [PATCH 15/26] remove deprecated tests --- modules/db/src/test/DateTest.scala | 29 ----------------------------- modules/db/src/test/DbApiTest.scala | 26 -------------------------- 2 files changed, 55 deletions(-) delete mode 100644 modules/db/src/test/DateTest.scala delete mode 100644 modules/db/src/test/DbApiTest.scala diff --git a/modules/db/src/test/DateTest.scala b/modules/db/src/test/DateTest.scala deleted file mode 100644 index 4edbd8932a..0000000000 --- a/modules/db/src/test/DateTest.scala +++ /dev/null @@ -1,29 +0,0 @@ -package lila.db - -import api._ -import Implicits._ -import org.joda.time.DateTime -import org.specs2.execute.{ Result, AsResult } -import org.specs2.mutable._ -import org.specs2.specification._ -import play.api.libs.json._ -import play.api.test._ -import reactivemongo.api._ -import reactivemongo.bson._ - -class DateTest extends Specification { - - val date = DateTime.now - import play.modules.reactivemongo.json.ImplicitBSONHandlers._ - - "date conversion" should { - "js to bson" in { - val doc = JsObjectWriter.write(Json.obj( - "ca" -> $gt($date(date)) - )) - doc.getAsTry[BSONDocument]("ca") flatMap { gt => - gt.getAsTry[BSONDateTime]("$gt") - } must_== scala.util.Success(BSONDateTime(date.getMillis)) - } - } -} diff --git a/modules/db/src/test/DbApiTest.scala b/modules/db/src/test/DbApiTest.scala deleted file mode 100644 index 0e67dbb2b5..0000000000 --- a/modules/db/src/test/DbApiTest.scala +++ /dev/null @@ -1,26 +0,0 @@ -package lila.db - -import org.joda.time.DateTime -import org.specs2.mutable._ -import play.api.libs.json._ -import play.api.test._ -import Types._ - -class DbApiTest extends Specification { - - import api._ - - val date = DateTime.now - - "operators" should { - - "$set" in { - $set("foo" -> "bar") must_== Json.obj( - "$set" -> Json.obj("foo" -> "bar")) - } - // "$set DateTime" in new WithApplication { - // $set("foo" -> date) must_== Json.obj( - // "$set" -> Json.obj("foo" -> Json.obj("$date" -> date.getMillis))) - // } - } -} From 9e184f2641b3f62d4d66a4abde8820fbbae7a3d9 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 00:44:28 +0700 Subject: [PATCH 16/26] make PimpedJson an AnyVal --- modules/common/src/main/PimpedJson.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/common/src/main/PimpedJson.scala b/modules/common/src/main/PimpedJson.scala index c5663a2f5b..79af9c7a32 100644 --- a/modules/common/src/main/PimpedJson.scala +++ b/modules/common/src/main/PimpedJson.scala @@ -2,11 +2,9 @@ package lila.common import play.api.libs.json._ -object PimpedJson extends PimpedJson +object PimpedJson { -trait PimpedJson { - - implicit final class LilaPimpedJsObject(js: JsObject) { + implicit final class LilaPimpedJsObject(val js: JsObject) extends AnyVal { def str(key: String): Option[String] = (js \ key).asOpt[String] @@ -41,7 +39,7 @@ trait PimpedJson { } } - implicit final class LilaPimpedJsValue(js: JsValue) { + implicit final class LilaPimpedJsValue(val js: JsValue) extends AnyVal { def str(key: String): Option[String] = js.asOpt[JsObject] flatMap { obj => From 838e2f1af617ef63faf42ae89760cd08f58acc6b Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 00:59:33 +0700 Subject: [PATCH 17/26] extract future extensions and make them an AnyVal --- modules/common/src/main/PackageObject.scala | 91 +--------------- modules/common/src/main/PimpedConfig.scala | 2 +- modules/common/src/main/PimpedFuture.scala | 109 ++++++++++++++++++++ 3 files changed, 112 insertions(+), 90 deletions(-) create mode 100644 modules/common/src/main/PimpedFuture.scala diff --git a/modules/common/src/main/PackageObject.scala b/modules/common/src/main/PackageObject.scala index aa34183892..1ae1ad0a30 100644 --- a/modules/common/src/main/PackageObject.scala +++ b/modules/common/src/main/PackageObject.scala @@ -125,95 +125,8 @@ trait WithPlay { self: PackageObject => Future sequence t } - implicit final class LilaPimpedFuture[A](fua: Fu[A]) { - - def >>-(sideEffect: => Unit): Fu[A] = fua andThen { - case _ => sideEffect - } - - def >>[B](fub: => Fu[B]): Fu[B] = fua flatMap (_ => fub) - - def void: Funit = fua map (_ => Unit) - - def inject[B](b: => B): Fu[B] = fua map (_ => b) - - def injectAnyway[B](b: => B): Fu[B] = fua.fold(_ => b, _ => b) - - def effectFold(fail: Exception => Unit, succ: A => Unit) { - fua onComplete { - case scala.util.Failure(e: Exception) => fail(e) - case scala.util.Failure(e) => throw e // Throwables - case scala.util.Success(e) => succ(e) - } - } - - def andThenAnyway(sideEffect: => Unit): Fu[A] = { - fua onComplete { - case scala.util.Failure(_) => sideEffect - case scala.util.Success(_) => sideEffect - } - fua - } - - def fold[B](fail: Exception => B, succ: A => B): Fu[B] = - fua map succ recover { case e: Exception => fail(e) } - - def flatFold[B](fail: Exception => Fu[B], succ: A => Fu[B]): Fu[B] = - fua flatMap succ recoverWith { case e: Exception => fail(e) } - - def logFailure(logger: => lila.log.Logger, msg: Exception => String): Fu[A] = - addFailureEffect { e => logger.warn(msg(e), e) } - def logFailure(logger: => lila.log.Logger): Fu[A] = logFailure(logger, _.toString) - - def addEffect(effect: A => Unit) = fua ~ (_ foreach effect) - - def addFailureEffect(effect: Exception => Unit) = fua ~ (_ onFailure { - case e: Exception => effect(e) - }) - - def addEffects(fail: Exception => Unit, succ: A => Unit): Fu[A] = - fua andThen { - case scala.util.Failure(e: Exception) => fail(e) - case scala.util.Failure(e) => throw e // Throwables - case scala.util.Success(e) => succ(e) - } - - def mapFailure(f: Exception => Exception) = fua recover { - case cause: Exception => throw f(cause) - } - - def prefixFailure(p: => String) = fua mapFailure { e => - common.LilaException(s"$p ${e.getMessage}") - } - - def thenPp: Fu[A] = fua ~ { - _.effectFold( - e => println("[failure] " + e), - a => println("[success] " + a) - ) - } - - def thenPp(msg: String): Fu[A] = fua ~ { - _.effectFold( - e => println(s"[$msg] [failure] $e"), - a => println(s"[$msg] [success] $a") - ) - } - - def awaitSeconds(seconds: Int): A = { - import scala.concurrent.duration._ - scala.concurrent.Await.result(fua, seconds.seconds) - } - - def withTimeout(duration: FiniteDuration, error: => Throwable)(implicit system: akka.actor.ActorSystem): Fu[A] = { - Future firstCompletedOf Seq(fua, - akka.pattern.after(duration, system.scheduler)(fufail(error))) - } - - def chronometer = lila.common.Chronometer(fua) - - def mon(path: lila.mon.RecPath) = chronometer.mon(path).result - } + implicit def LilaPimpedFuture[A](fua: Fu[A]): PimpedFuture.LilaPimpedFuture[A] = + new PimpedFuture.LilaPimpedFuture(fua) implicit final class LilaPimpedFutureZero[A: Zero](fua: Fu[A]) { diff --git a/modules/common/src/main/PimpedConfig.scala b/modules/common/src/main/PimpedConfig.scala index 975ae81072..4674f0cf16 100644 --- a/modules/common/src/main/PimpedConfig.scala +++ b/modules/common/src/main/PimpedConfig.scala @@ -7,7 +7,7 @@ import com.typesafe.config.Config object PimpedConfig { - implicit final class LilaPimpedConfig(config: Config) { + implicit final class LilaPimpedConfig(val config: Config) extends AnyVal { def millis(name: String): Int = config.getDuration(name, TimeUnit.MILLISECONDS).toInt def seconds(name: String): Int = config.getDuration(name, TimeUnit.SECONDS).toInt diff --git a/modules/common/src/main/PimpedFuture.scala b/modules/common/src/main/PimpedFuture.scala new file mode 100644 index 0000000000..94713a8f97 --- /dev/null +++ b/modules/common/src/main/PimpedFuture.scala @@ -0,0 +1,109 @@ +package lila + +import play.api.libs.concurrent.Execution.Implicits._ +import scala.concurrent.Future +import scala.concurrent.duration._ + +object PimpedFuture { + + private type Fu[A] = Future[A] + private type Funit = Fu[Unit] + + final class LilaPimpedFuture[A](val fua: Fu[A]) extends AnyVal { + + def >>-(sideEffect: => Unit): Fu[A] = fua andThen { + case _ => sideEffect + } + + def >>[B](fub: => Fu[B]): Fu[B] = fua flatMap (_ => fub) + + def void: Funit = fua map (_ => Unit) + + def inject[B](b: => B): Fu[B] = fua map (_ => b) + + def injectAnyway[B](b: => B): Fu[B] = fold(_ => b, _ => b) + + def effectFold(fail: Exception => Unit, succ: A => Unit) { + fua onComplete { + case scala.util.Failure(e: Exception) => fail(e) + case scala.util.Failure(e) => throw e // Throwables + case scala.util.Success(e) => succ(e) + } + } + + def andThenAnyway(sideEffect: => Unit): Fu[A] = { + fua onComplete { + case scala.util.Failure(_) => sideEffect + case scala.util.Success(_) => sideEffect + } + fua + } + + def fold[B](fail: Exception => B, succ: A => B): Fu[B] = + fua map succ recover { case e: Exception => fail(e) } + + def flatFold[B](fail: Exception => Fu[B], succ: A => Fu[B]): Fu[B] = + fua flatMap succ recoverWith { case e: Exception => fail(e) } + + def logFailure(logger: => lila.log.Logger, msg: Exception => String): Fu[A] = + addFailureEffect { e => logger.warn(msg(e), e) } + def logFailure(logger: => lila.log.Logger): Fu[A] = logFailure(logger, _.toString) + + def addEffect(effect: A => Unit) = { + fua foreach effect + fua + } + + def addFailureEffect(effect: Exception => Unit) = { + fua onFailure { + case e: Exception => effect(e) + } + fua + } + + def addEffects(fail: Exception => Unit, succ: A => Unit): Fu[A] = + fua andThen { + case scala.util.Failure(e: Exception) => fail(e) + case scala.util.Failure(e) => throw e // Throwables + case scala.util.Success(e) => succ(e) + } + + def mapFailure(f: Exception => Exception) = fua recover { + case cause: Exception => throw f(cause) + } + + def prefixFailure(p: => String) = mapFailure { e => + common.LilaException(s"$p ${e.getMessage}") + } + + def thenPp: Fu[A] = { + effectFold( + e => println("[failure] " + e), + a => println("[success] " + a) + ) + fua + } + + def thenPp(msg: String): Fu[A] = { + effectFold( + e => println(s"[$msg] [failure] $e"), + a => println(s"[$msg] [success] $a") + ) + fua + } + + def awaitSeconds(seconds: Int): A = { + import scala.concurrent.duration._ + scala.concurrent.Await.result(fua, seconds.seconds) + } + + def withTimeout(duration: FiniteDuration, error: => Throwable)(implicit system: akka.actor.ActorSystem): Fu[A] = { + Future firstCompletedOf Seq(fua, + akka.pattern.after(duration, system.scheduler)(Future failed error)) + } + + def chronometer = lila.common.Chronometer(fua) + + def mon(path: lila.mon.RecPath) = chronometer.mon(path).result + } +} From c562234c1f6c0b61224c6767205b14c5acce7743 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 10:15:22 +0700 Subject: [PATCH 18/26] site actor compiles --- app/controllers/LilaController.scala | 8 ++ app/controllers/Main.scala | 9 ++ modules/site/src/main/Env.scala | 2 +- modules/site/src/main/Socket.scala | 7 +- modules/site/src/main/SocketHandler.scala | 15 +- modules/site/src/main/actorApi.scala | 1 + modules/socket/src/main/Handler.scala | 132 ++++++++++-------- .../socket/src/main/SocketMemberActor.scala | 15 +- modules/socket/src/main/WithSocket.scala | 2 +- modules/user/src/main/UserRepo.scala | 2 +- 10 files changed, 112 insertions(+), 81 deletions(-) diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 59e4fc300c..fe215bc5f4 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -70,6 +70,14 @@ private[controllers] trait LilaController } } + protected def NewSocketOption[In, Out](f: Context => Fu[Option[Flow[In, Out, _]]])(implicit transformer: MessageFlowTransformer[In, Out]): WebSocket = { + WebSocket.acceptOrResult[In, Out] { req => + reqToCtx(req) flatMap f map { + case None => Left(NotFound(jsonError("socket resource not found"))) + case Some(pair) => Right(pair) + } + } + protected def Open(f: Context => Fu[Result]): Action[Unit] = Open(BodyParsers.parse.empty)(f) diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala index 266981cfbd..4f52c4dbd7 100644 --- a/app/controllers/Main.scala +++ b/app/controllers/Main.scala @@ -41,6 +41,15 @@ object Main extends LilaController { } } + def newWebsocket = NewSocketOption { implicit ctx => + get("sri") ?? { uid => + // Env.site.socketHandler(uid, ctx.userId, get("flag")) map some + ActorFlow.actorRef { out => + lila.socket.SocketMemberActor.props(out) + ) + } + } + def captchaCheck(id: String) = Open { implicit ctx => Env.hub.actor.captcher ? ValidCaptcha(id, ~get("solution")) map { case valid: Boolean => Ok(valid fold (1, 0)) diff --git a/modules/site/src/main/Env.scala b/modules/site/src/main/Env.scala index bff39ae737..f83f35f192 100644 --- a/modules/site/src/main/Env.scala +++ b/modules/site/src/main/Env.scala @@ -17,7 +17,7 @@ final class Env( private val socket = system.actorOf( Props(new Socket(timeout = SocketUidTtl)), name = SocketName) - lazy val socketHandler = new SocketHandler(socket, hub) + lazy val socketHandler = new SocketHandler(socket, hub)(system) } object Env { diff --git a/modules/site/src/main/Socket.scala b/modules/site/src/main/Socket.scala index 72bf7a2ed5..8163be4188 100644 --- a/modules/site/src/main/Socket.scala +++ b/modules/site/src/main/Socket.scala @@ -16,12 +16,7 @@ private[site] final class Socket(timeout: Duration) extends SocketActor[Member]( def receiveSpecific = { - case Join(uid, username, tags) => { - val (enumerator, channel) = Concurrent.broadcast[JsValue] - val member = Member(channel, username, tags) - addMember(uid, member) - sender ! Connected(enumerator, member) - } + case AddMember(uid, member) => addMember(uid, member) case SendToFlag(flag, message) => members.values filter (_ hasFlag flag) foreach { diff --git a/modules/site/src/main/SocketHandler.scala b/modules/site/src/main/SocketHandler.scala index 5ce078c98a..8d8eabe2d4 100644 --- a/modules/site/src/main/SocketHandler.scala +++ b/modules/site/src/main/SocketHandler.scala @@ -4,6 +4,7 @@ import scala.concurrent.duration._ import akka.actor._ import play.api.libs.json._ +import play.api.libs.streams.ActorFlow import actorApi._ import lila.common.PimpedJson._ @@ -12,15 +13,17 @@ import lila.socket.actorApi.StartWatching private[site] final class SocketHandler( socket: ActorRef, - hub: lila.hub.Env) { + hub: lila.hub.Env)(implicit val system: ActorSystem) { def apply( uid: String, userId: Option[String], - flag: Option[String]): Fu[JsSocketHandler] = { - - Handler(hub, socket, uid, Join(uid, userId, flag), userId) { - case Connected(enum, member) => (Handler.emptyController, enum, member) + flag: Option[String]): JsFlow = + ActorFlow.actorRef[JsValue, JsValue] { out => + val member = Member(out, userId, flag) + socket ! AddMember(uid, member) + val controller = Handler.completeController(hub, socket, member, uid, userId)(Handler.emptyController) + lila.socket.SocketMemberActor.props(out, controller) } - } + } diff --git a/modules/site/src/main/actorApi.scala b/modules/site/src/main/actorApi.scala index dda4be8d2d..6008232123 100644 --- a/modules/site/src/main/actorApi.scala +++ b/modules/site/src/main/actorApi.scala @@ -16,4 +16,5 @@ case class Member( } case class Join(uid: String, userId: Option[String], flag: Option[String]) +case class AddMember(uid: String, member: Member) private[site] case class Connected(enumerator: JsEnumerator, member: Member) diff --git a/modules/socket/src/main/Handler.scala b/modules/socket/src/main/Handler.scala index 091404b9f8..bc6ebfbe77 100644 --- a/modules/socket/src/main/Handler.scala +++ b/modules/socket/src/main/Handler.scala @@ -12,6 +12,7 @@ import scala.concurrent.duration._ import actorApi._ import lila.common.PimpedJson._ import lila.hub.actorApi.relation.ReloadOnlineFriends +import lila.socket.Socket.makeMessage import makeTimeout.large import Step.openingWriter @@ -24,68 +25,88 @@ object Handler { lazy val AnaRateLimit = new lila.memo.RateLimit(90, 60 seconds, "socket analysis move") - def apply( + def completeController( hub: lila.hub.Env, socket: ActorRef, + member: SocketMember, uid: String, - join: Any, - userId: Option[String])(connecter: Connecter): Fu[JsFlow] = { - - def baseController(member: SocketMember): Controller = { - case ("p", _) => socket ! Ping(uid) - case ("following_onlines", _) => userId foreach { u => - hub.actor.relation ! ReloadOnlineFriends(u) - } - case ("startWatching", o) => o str "d" foreach { ids => - hub.actor.moveBroadcast ! StartWatching(uid, member, ids.split(' ').toSet) - } - case ("moveLat", o) => hub.channel.roundMoveTime ! (~(o boolean "d")).fold( - Channel.Sub(member), - Channel.UnSub(member)) - case ("anaMove", o) => AnaRateLimit(uid) { - AnaMove parse o foreach { anaMove => - anaMove.step match { - case scalaz.Success(step) => - member push lila.socket.Socket.makeMessage("step", Json.obj( - "step" -> step.toJson, - "path" -> anaMove.path - )) - case scalaz.Failure(err) => - member push lila.socket.Socket.makeMessage("stepFailure", err.toString) - } + userId: Option[String])(controller: Controller): JsValue => Unit = { + val control = controller orElse baseController(hub, socket, member, uid, userId) + in => + in.asOpt[JsObject] foreach { obj => + obj str "t" foreach { t => + control.lift(t -> obj) } } - case ("anaDrop", o) => AnaRateLimit(uid) { - AnaDrop parse o foreach { anaDrop => - anaDrop.step match { - case scalaz.Success(step) => - member push lila.socket.Socket.makeMessage("step", Json.obj( - "step" -> step.toJson, - "path" -> anaDrop.path - )) - case scalaz.Failure(err) => - member push lila.socket.Socket.makeMessage("stepFailure", err.toString) - } + } + + def baseController( + hub: lila.hub.Env, + socket: ActorRef, + member: SocketMember, + uid: String, + userId: Option[String]): Controller = { + case ("p", _) => socket ! Ping(uid) + case ("following_onlines", _) => userId foreach { u => + hub.actor.relation ! ReloadOnlineFriends(u) + } + case ("startWatching", o) => o str "d" foreach { ids => + hub.actor.moveBroadcast ! StartWatching(uid, member, ids.split(' ').toSet) + } + case ("moveLat", o) => hub.channel.roundMoveTime ! (~(o boolean "d")).fold( + Channel.Sub(member), + Channel.UnSub(member)) + case ("anaMove", o) => AnaRateLimit(uid) { + AnaMove parse o foreach { anaMove => + anaMove.step match { + case scalaz.Success(step) => + member push makeMessage("step", Json.obj( + "step" -> step.toJson, + "path" -> anaMove.path + )) + case scalaz.Failure(err) => + member push makeMessage("stepFailure", err.toString) } } - case ("anaDests", o) => AnaRateLimit(uid) { - AnaDests parse o match { - case Some(req) => - member push lila.socket.Socket.makeMessage("dests", Json.obj( - "dests" -> req.dests, - "path" -> req.path - ) ++ req.opening.?? { o => - Json.obj("opening" -> o) - }) - case None => - member push lila.socket.Socket.makeMessage("destsFailure", "Bad dests request") + } + case ("anaDrop", o) => AnaRateLimit(uid) { + AnaDrop parse o foreach { anaDrop => + anaDrop.step match { + case scalaz.Success(step) => + member push makeMessage("step", Json.obj( + "step" -> step.toJson, + "path" -> anaDrop.path + )) + case scalaz.Failure(err) => + member push makeMessage("stepFailure", err.toString) } } - case _ => // logwarn("Unhandled msg: " + msg) } + case ("anaDests", o) => AnaRateLimit(uid) { + AnaDests parse o match { + case Some(req) => + member push makeMessage("dests", Json.obj( + "dests" -> req.dests, + "path" -> req.path + ) ++ req.opening.?? { o => + Json.obj("opening" -> o) + }) + case None => + member push makeMessage("destsFailure", "Bad dests request") + } + } + case _ => // logwarn("Unhandled msg: " + msg) + } + + def apply( + hub: lila.hub.Env, + socket: ActorRef, + uid: String, + join: Any, + userId: Option[String])(connecter: Connecter): Fu[JsSocketHandler] = { def iteratee(controller: Controller, member: SocketMember): JsIteratee = { - val control = controller orElse baseController(member) + val control = controller orElse baseController(hub, socket, member, uid, userId) Iteratee.foreach[JsValue](jsv => jsv.asOpt[JsObject] foreach { obj => obj str "t" foreach { t => @@ -95,17 +116,6 @@ object Handler { ).map(_ => socket ! Quit(uid)) } - def sink(controller: Controller, member: SocketMember) = { - val control = controller orElse baseController(member) - Sink.foreach[JsValue] { jsv => - jsv.asOpt[JsObject] foreach { obj => - obj str "t" foreach { t => - control.lift(t -> obj) - } - } - } - } - socket ? join map connecter map { case (controller, enum, member) => iteratee(controller, member) -> enum } diff --git a/modules/socket/src/main/SocketMemberActor.scala b/modules/socket/src/main/SocketMemberActor.scala index eadf65f6dd..c47fb53e7e 100644 --- a/modules/socket/src/main/SocketMemberActor.scala +++ b/modules/socket/src/main/SocketMemberActor.scala @@ -3,20 +3,25 @@ package lila.socket import akka.actor._ import play.api.libs.json.JsValue -final class SocketMemberActor(out: ActorRef) extends Actor { +// implements ActorFlow.actorRef out actor +final class SocketMemberActor( + out: ActorRef, + handler: JsValue => Unit) extends Actor { import SocketMemberActor._ def receive = { - case Out(msg) => out ! msg + // case Send(msg) => out ! msg - case msg: JsValue => - // out ! ("I received your message: " + msg) + case msg: JsValue => handler(msg) } } object SocketMemberActor { - case class Out(msg: JsValue) + // case class Send(msg: JsValue) + + def props(out: ActorRef, handler: JsValue => Unit) = + Props(new SocketMemberActor(out, handler)) } diff --git a/modules/socket/src/main/WithSocket.scala b/modules/socket/src/main/WithSocket.scala index 2bac2478d8..a62fb4aefa 100644 --- a/modules/socket/src/main/WithSocket.scala +++ b/modules/socket/src/main/WithSocket.scala @@ -15,7 +15,7 @@ trait WithSocket { type JsIteratee = Iteratee[JsValue, _] type JsSocketHandler = (JsIteratee, JsEnumerator) - type JsFlow = Flow[JsValue, JsValue, NotUsed] + type JsFlow = Flow[JsValue, JsValue, _] // type JsSource = Source[JsValue, NotUsed] // type JsSink = Sink[JsValue, NotUsed] } diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index e2c5dc3f73..ed95ed5216 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -233,7 +233,7 @@ object UserRepo { import java.util.regex.Matcher.quoteReplacement val escaped = """^([\w-]*).*$""".r.replaceAllIn(normalize(username), m => quoteReplacement(m group 1)) val regex = "^" + escaped + ".*$" - coll.find($id($regex(regex, "")), $doc(F.username -> true)) + coll.find($doc("_id".$regex(regex, "")), $doc(F.username -> true)) .sort($sort desc "_id") .cursor[Bdoc]().gather[List](max) .map { From 61ce0baa89a6677dffc521114c7dd389d8b17e45 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 12:15:52 +0700 Subject: [PATCH 19/26] upgrade akka --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 814b96640d..52890c6819 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -53,7 +53,7 @@ object Dependencies { val util = "io.spray" %% "spray-util" % version } object akka { - val version = "2.4.2" + val version = "2.4.3" val actor = "com.typesafe.akka" %% "akka-actor" % version val slf4j = "com.typesafe.akka" %% "akka-slf4j" % version val stream = "com.typesafe.akka" %% "akka-stream" % version From 91391d528223277c62a5848f67c35043bd32506e Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 12:15:58 +0700 Subject: [PATCH 20/26] more rewrite of websocket code for akka streams --- app/controllers/Challenge.scala | 2 +- app/controllers/LilaController.scala | 53 +++++++++++++------ app/controllers/LilaSocket.scala | 6 +-- app/controllers/Lobby.scala | 2 +- app/controllers/Main.scala | 11 +--- app/controllers/Round.scala | 4 +- app/controllers/Simul.scala | 2 +- app/controllers/Tournament.scala | 2 +- modules/challenge/src/main/Env.scala | 2 +- modules/challenge/src/main/Socket.scala | 18 +++---- .../challenge/src/main/SocketHandler.scala | 26 ++++----- modules/lobby/src/main/Env.scala | 2 +- modules/lobby/src/main/Socket.scala | 9 +--- modules/lobby/src/main/SocketHandler.scala | 19 +++---- modules/lobby/src/main/actorApi.scala | 11 ++-- modules/round/src/main/Env.scala | 2 +- modules/round/src/main/Socket.scala | 11 ++-- modules/round/src/main/SocketHandler.scala | 33 +++++------- modules/round/src/main/actorApi.scala | 21 +++----- modules/simul/src/main/Env.scala | 5 +- modules/simul/src/main/Socket.scala | 8 +-- modules/simul/src/main/SocketHandler.scala | 45 ++++++---------- modules/simul/src/main/actorApi.scala | 15 +++--- modules/site/src/main/SocketHandler.scala | 16 ++---- modules/site/src/main/actorApi.scala | 5 +- modules/socket/src/main/Channel.scala | 4 +- modules/socket/src/main/Handler.scala | 47 +++++----------- modules/socket/src/main/SocketMember.scala | 14 ++--- .../socket/src/main/SocketMemberActor.scala | 10 ++-- modules/socket/src/main/WithSocket.scala | 19 ++----- modules/socket/src/main/actorApi.scala | 4 -- modules/tournament/src/main/Env.scala | 5 +- modules/tournament/src/main/Socket.scala | 8 +-- .../tournament/src/main/SocketHandler.scala | 49 ++++++----------- modules/tournament/src/main/actorApi.scala | 13 +++-- 35 files changed, 199 insertions(+), 304 deletions(-) diff --git a/app/controllers/Challenge.scala b/app/controllers/Challenge.scala index 83870d0274..4cd5664b53 100644 --- a/app/controllers/Challenge.scala +++ b/app/controllers/Challenge.scala @@ -137,7 +137,7 @@ object Challenge extends LilaController { } } - def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx => + def websocket(id: String, apiVersion: Int) = SocketOption { implicit ctx => env.api byId id flatMap { _ ?? { c => get("sri") ?? { uid => diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index fe215bc5f4..428b68263a 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -1,14 +1,13 @@ package controllers +import akka.stream.scaladsl._ import ornicar.scalalib.Zero import play.api.data.Form import play.api.http._ -import play.api.libs.iteratee.{ Iteratee, Enumerator } import play.api.libs.json.{ Json, JsValue, JsObject, JsArray, Writes } import play.api.mvc._, Results._ -import play.api.mvc.WebSocket.FrameFormatter +import play.api.mvc.WebSocket.MessageFlowTransformer import play.twirl.api.Html -import scalaz.Monoid import lila.api.{ PageData, Context, HeaderContext, BodyContext, TokenBucket } import lila.app._ @@ -48,30 +47,50 @@ private[controllers] trait LilaController CACHE_CONTROL -> "no-cache, no-store, must-revalidate", EXPIRES -> "0" ) - protected def Socket[A: FrameFormatter](f: Context => Fu[(Iteratee[A, _], Enumerator[A])]) = - WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f map scala.util.Right.apply } - - protected def SocketEither[A: FrameFormatter](f: Context => Fu[Either[Result, (Iteratee[A, _], Enumerator[A])]]) = - WebSocket.tryAccept[A] { req => reqToCtx(req) flatMap f } + private implicit val jsonMessageFlowTransformer: MessageFlowTransformer[JsObject, JsObject] = { + import scala.util.control.NonFatal + import play.api.libs.streams.AkkaStreams + import websocket._ + def closeOnException[T](block: => Option[T]) = try { + Left(block getOrElse { + sys error "Not a JsObject" + }) + } + catch { + case NonFatal(e) => Right(CloseMessage(Some(CloseCodes.Unacceptable), + "Unable to parse json message")) + } - protected def SocketOption[A: FrameFormatter](f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) = - WebSocket.tryAccept[A] { req => - reqToCtx(req) flatMap f map { - case None => Left(NotFound(jsonError("socket resource not found"))) - case Some(pair) => Right(pair) + new MessageFlowTransformer[JsObject, JsObject] { + def transform(flow: Flow[JsObject, JsObject, _]) = { + AkkaStreams.bypassWith[Message, JsObject, Message](Flow[Message].collect { + case BinaryMessage(data) => closeOnException(Json.parse(data.iterator.asInputStream).asOpt[JsObject]) + case TextMessage(text) => closeOnException(Json.parse(text).asOpt[JsObject]) + })(flow map { json => TextMessage(Json.stringify(json)) }) } } + } + + protected def Socket(f: Context => Fu[JsFlow]) = + WebSocket.acceptOrResult[JsObject, JsObject] { req => + reqToCtx(req) flatMap f map Right.apply + } - protected def SocketOptionLimited[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) = + protected def SocketEither(f: Context => Fu[Either[Result, JsFlow]]) = + WebSocket.acceptOrResult[JsObject, JsObject] { req => + reqToCtx(req) flatMap f + } + + protected def SocketOptionLimited(consumer: TokenBucket.Consumer, name: String)(f: Context => Fu[Option[JsFlow]]) = rateLimitedSocket[A](consumer, name) { ctx => f(ctx) map { case None => Left(NotFound(jsonError("socket resource not found"))) - case Some(pair) => Right(pair) + case Some(flow) => Right(flow) } } - protected def NewSocketOption[In, Out](f: Context => Fu[Option[Flow[In, Out, _]]])(implicit transformer: MessageFlowTransformer[In, Out]): WebSocket = { - WebSocket.acceptOrResult[In, Out] { req => + protected def SocketOption(f: Context => Fu[Option[JsFlow]]): WebSocket = + WebSocket.acceptOrResult[JsObject, JsObject] { req => reqToCtx(req) flatMap f map { case None => Left(NotFound(jsonError("socket resource not found"))) case Some(pair) => Right(pair) diff --git a/app/controllers/LilaSocket.scala b/app/controllers/LilaSocket.scala index 51941d8144..88dea834a7 100644 --- a/app/controllers/LilaSocket.scala +++ b/app/controllers/LilaSocket.scala @@ -12,12 +12,12 @@ import lila.common.HTTPRequest trait LilaSocket { self: LilaController => - private type AcceptType[A] = Context => Fu[Either[Result, (Iteratee[A, _], Enumerator[A])]] + private type AcceptType[A] = Context => Fu[Either[Result, JsFlow]] private val logger = lila.log("ratelimit") - def rateLimitedSocket[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket = - WebSocket.tryAccept[A] { req => + def rateLimitedSocket(consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket = + WebSocket.acceptOrResult[A, A] { req => reqToCtx(req) flatMap { ctx => val ip = HTTPRequest lastRemoteAddress req def userInfo = { diff --git a/app/controllers/Lobby.scala b/app/controllers/Lobby.scala index 24b2b1d4d3..ca844a3a2f 100644 --- a/app/controllers/Lobby.scala +++ b/app/controllers/Lobby.scala @@ -48,7 +48,7 @@ object Lobby extends LilaController { size = 10, rate = 6) - def socket(apiVersion: Int) = SocketOptionLimited[JsValue](socketConsumer, "lobby") { implicit ctx => + def socket(apiVersion: Int) = SocketOptionLimited(socketConsumer, "lobby") { implicit ctx => get("sri") ?? { uid => Env.lobby.socketHandler( uid = uid, diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala index 4f52c4dbd7..ec4bd90614 100644 --- a/app/controllers/Main.scala +++ b/app/controllers/Main.scala @@ -37,16 +37,7 @@ object Main extends LilaController { def websocket = SocketOption { implicit ctx => get("sri") ?? { uid => - Env.site.socketHandler(uid, ctx.userId, get("flag")) map some - } - } - - def newWebsocket = NewSocketOption { implicit ctx => - get("sri") ?? { uid => - // Env.site.socketHandler(uid, ctx.userId, get("flag")) map some - ActorFlow.actorRef { out => - lila.socket.SocketMemberActor.props(out) - ) + fuccess(Env.site.socketHandler(uid, ctx.userId, get("flag"))) } } diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index 3e69d0b1e1..97b0937676 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -23,7 +23,7 @@ object Round extends LilaController with TheftPrevention { private def bookmarkApi = Env.bookmark.api private def analyser = Env.analyse.analyser - def websocketWatcher(gameId: String, color: String) = SocketOption[JsValue] { implicit ctx => + def websocketWatcher(gameId: String, color: String) = SocketOption { implicit ctx => get("sri") ?? { uid => env.socketHandler.watcher( gameId = gameId, @@ -35,7 +35,7 @@ object Round extends LilaController with TheftPrevention { } } - def websocketPlayer(fullId: String, apiVersion: Int) = SocketEither[JsValue] { implicit ctx => + def websocketPlayer(fullId: String, apiVersion: Int) = SocketEither { implicit ctx => GameRepo pov fullId flatMap { case Some(pov) => if (isTheft(pov)) fuccess(Left(theftResponse)) diff --git a/app/controllers/Simul.scala b/app/controllers/Simul.scala index b18f11c7d2..18e40a5905 100644 --- a/app/controllers/Simul.scala +++ b/app/controllers/Simul.scala @@ -114,7 +114,7 @@ object Simul extends LilaController { } } - def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx => + def websocket(id: String, apiVersion: Int) = SocketOption { implicit ctx => get("sri") ?? { uid => env.socketHandler.join(id, uid, ctx.me) } diff --git a/app/controllers/Tournament.scala b/app/controllers/Tournament.scala index 38cf412970..34b622cb22 100644 --- a/app/controllers/Tournament.scala +++ b/app/controllers/Tournament.scala @@ -172,7 +172,7 @@ object Tournament extends LilaController { } } - def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx => + def websocket(id: String, apiVersion: Int) = SocketOption { implicit ctx => get("sri") ?? { uid => env.socketHandler.join(id, uid, ctx.me) } diff --git a/modules/challenge/src/main/Env.scala b/modules/challenge/src/main/Env.scala index 6b063aee01..c9f5880820 100644 --- a/modules/challenge/src/main/Env.scala +++ b/modules/challenge/src/main/Env.scala @@ -45,7 +45,7 @@ final class Env( lazy val socketHandler = new SocketHandler( hub = hub, socketHub = socketHub, - pingChallenge = api.ping) + pingChallenge = api.ping)(system) lazy val api = new ChallengeApi( repo = repo, diff --git a/modules/challenge/src/main/Socket.scala b/modules/challenge/src/main/Socket.scala index 24cf3ca3db..2a52fa1d1d 100644 --- a/modules/challenge/src/main/Socket.scala +++ b/modules/challenge/src/main/Socket.scala @@ -1,12 +1,11 @@ package lila.challenge import akka.actor._ -import play.api.libs.iteratee.Concurrent import play.api.libs.json._ import scala.concurrent.duration.Duration import lila.hub.TimeBomb -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.{ SocketActor, History, Historical } private final class Socket( @@ -40,15 +39,11 @@ private final class Socket( if (timeBomb.boom) self ! PoisonPill } - case GetVersion => sender ! history.version + case GetVersion => sender ! history.version - case Socket.Join(uid, userId, owner) => - val (enumerator, channel) = Concurrent.broadcast[JsValue] - val member = Socket.Member(channel, userId, owner) - addMember(uid, member) - sender ! Socket.Connected(enumerator, member) + case Socket.AddMember(uid, member) => addMember(uid, member) - case Quit(uid) => quit(uid) + case Quit(uid) => quit(uid) } protected def shouldSkipMessageFor(message: Message, member: Socket.Member) = false @@ -57,14 +52,13 @@ private final class Socket( private object Socket { case class Member( - channel: JsChannel, + out: ActorRef, userId: Option[String], owner: Boolean) extends lila.socket.SocketMember { val troll = false } - case class Join(uid: String, userId: Option[String], owner: Boolean) - case class Connected(enumerator: JsEnumerator, member: Member) + case class AddMember(uid: String, member: Member) case object Reload } diff --git a/modules/challenge/src/main/SocketHandler.scala b/modules/challenge/src/main/SocketHandler.scala index 2d05025ff0..031d4b4102 100644 --- a/modules/challenge/src/main/SocketHandler.scala +++ b/modules/challenge/src/main/SocketHandler.scala @@ -1,14 +1,11 @@ package lila.challenge -import scala.concurrent.duration._ - import akka.actor._ import akka.pattern.ask -import akka.actor.ActorSelection import lila.common.PimpedJson._ import lila.hub.actorApi.map._ -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.Handler import lila.user.User import makeTimeout.short @@ -16,20 +13,25 @@ import makeTimeout.short private[challenge] final class SocketHandler( hub: lila.hub.Env, socketHub: ActorRef, - pingChallenge: Challenge.ID => Funit) { + pingChallenge: Challenge.ID => Funit)(implicit system: ActorSystem) { def join( challengeId: Challenge.ID, uid: String, userId: Option[User.ID], - owner: Boolean): Fu[Option[JsSocketHandler]] = for { - socket ← socketHub ? Get(challengeId) mapTo manifest[ActorRef] - join = Socket.Join(uid = uid, userId = userId, owner = owner) - handler ← Handler(hub, socket, uid, join, userId) { - case Socket.Connected(enum, member) => - (controller(socket, challengeId, uid, member), enum, member) + owner: Boolean): Fu[Option[JsFlow]] = + socketHub ? Get(challengeId) mapTo manifest[ActorRef] map { socket => + Handler.actorRef { out => + val member = Socket.Member(out, userId, owner) + socket ! Socket.AddMember(uid, member) + Handler.props(out)(hub, socket, member, uid, userId) { + case ("p", o) => o int "v" foreach { v => + socket ! PingVersion(uid, v) + } + case ("ping", _) if member.owner => pingChallenge(challengeId) + } + }.some } - } yield handler.some private def controller( socket: ActorRef, diff --git a/modules/lobby/src/main/Env.scala b/modules/lobby/src/main/Env.scala index 84374fc0fb..7bc75024fd 100644 --- a/modules/lobby/src/main/Env.scala +++ b/modules/lobby/src/main/Env.scala @@ -59,7 +59,7 @@ final class Env( hub = hub, lobby = lobby, socket = socket, - blocking = blocking) + blocking = blocking)(system) lazy val history = new History[actorApi.Messadata](ttl = MessageTtl) diff --git a/modules/lobby/src/main/Socket.scala b/modules/lobby/src/main/Socket.scala index c5d385f7f5..af84fd63fb 100644 --- a/modules/lobby/src/main/Socket.scala +++ b/modules/lobby/src/main/Socket.scala @@ -5,7 +5,6 @@ import scala.concurrent.Future import akka.actor._ import akka.pattern.ask -import play.api.libs.iteratee._ import play.api.libs.json._ import play.twirl.api.Html @@ -16,7 +15,7 @@ import lila.game.AnonCookie import lila.hub.actorApi.game.ChangeFeatured import lila.hub.actorApi.lobby._ import lila.hub.actorApi.timeline._ -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.{ SocketActor, History, Historical } import makeTimeout.short @@ -52,11 +51,7 @@ private[lobby] final class Socket( } } - case Join(uid, user, blocks, mobile) => - val (enumerator, channel) = Concurrent.broadcast[JsValue] - val member = Member(channel, user, blocks, uid, mobile) - addMember(uid, member) - sender ! Connected(enumerator, member) + case AddMember(uid, member) => addMember(uid, member) case ReloadTournaments(html) => notifyAllAsync(makeMessage("tournaments", html)) diff --git a/modules/lobby/src/main/SocketHandler.scala b/modules/lobby/src/main/SocketHandler.scala index 63bd02b21f..93325388c3 100644 --- a/modules/lobby/src/main/SocketHandler.scala +++ b/modules/lobby/src/main/SocketHandler.scala @@ -2,14 +2,13 @@ package lila.lobby import akka.actor._ import akka.pattern.{ ask, pipe } -import play.api.libs.iteratee._ import play.api.libs.json._ import scala.concurrent.duration._ import actorApi._ import lila.common.PimpedJson._ import lila.hub.actorApi.lobby._ -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.Handler import lila.user.User import makeTimeout.short @@ -18,7 +17,7 @@ private[lobby] final class SocketHandler( hub: lila.hub.Env, lobby: ActorRef, socket: ActorRef, - blocking: String => Fu[Set[String]]) { + blocking: String => Fu[Set[String]])(implicit system: ActorSystem) { private def controller( socket: ActorRef, @@ -40,12 +39,14 @@ private[lobby] final class SocketHandler( } lobby ! CancelSeek(id, user) } - def apply(uid: String, user: Option[User], mobile: Boolean): Fu[JsSocketHandler] = - (user ?? (u => blocking(u.id))) flatMap { blockedUserIds => - val join = Join(uid = uid, user = user, blocking = blockedUserIds, mobile = mobile) - Handler(hub, socket, uid, join, user map (_.id)) { - case Connected(enum, member) => - (controller(socket, uid, member), enum, member) + def apply(uid: String, user: Option[User], mobile: Boolean): Fu[JsFlow] = + (user ?? (u => blocking(u.id))) map { blockedUserIds => + Handler.actorRef { out => + val member = Member(out, user, blockedUserIds, uid, mobile) + socket ! AddMember(uid, member) + Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + controller(socket, uid, member) + } } } } diff --git a/modules/lobby/src/main/actorApi.scala b/modules/lobby/src/main/actorApi.scala index 8b79e18197..f36d17cdb4 100644 --- a/modules/lobby/src/main/actorApi.scala +++ b/modules/lobby/src/main/actorApi.scala @@ -1,6 +1,8 @@ package lila.lobby package actorApi +import akka.actor.ActorRef + import lila.game.Game import lila.socket.SocketMember import lila.user.User @@ -29,7 +31,7 @@ private[lobby] object LobbyUser { } private[lobby] case class Member( - channel: JsChannel, + out: ActorRef, user: Option[LobbyUser], uid: String, mobile: Boolean) extends SocketMember { @@ -40,8 +42,8 @@ private[lobby] case class Member( private[lobby] object Member { - def apply(channel: JsChannel, user: Option[User], blocking: Set[String], uid: String, mobile: Boolean): Member = Member( - channel = channel, + def apply(out: ActorRef, user: Option[User], blocking: Set[String], uid: String, mobile: Boolean): Member = Member( + out = out, user = user map { LobbyUser.make(_, blocking) }, uid = uid, mobile = mobile) @@ -51,7 +53,6 @@ private[lobby] case class HookMeta(hookId: Option[String] = None) private[lobby] case class Messadata(hook: Option[Hook] = None) -private[lobby] case class Connected(enumerator: JsEnumerator, member: Member) private[lobby] case class WithHooks(op: Iterable[String] => Unit) private[lobby] case class SaveHook(msg: AddHook) private[lobby] case class SaveSeek(msg: AddSeek) @@ -64,7 +65,7 @@ private[lobby] case class BiteHook(hookId: String, uid: String, user: Option[Lob private[lobby] case class BiteSeek(seekId: String, user: LobbyUser) private[lobby] case class JoinHook(uid: String, hook: Hook, game: Game, creatorColor: chess.Color) private[lobby] case class JoinSeek(userId: String, seek: Seek, game: Game, creatorColor: chess.Color) -private[lobby] case class Join(uid: String, user: Option[User], blocking: Set[String], mobile: Boolean) +private[lobby] case class AddMember(uid: String, member: Member) private[lobby] case object Resync private[lobby] case class HookIds(ids: Vector[String]) diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala index 45dc6842cd..57ff57ffc8 100644 --- a/modules/round/src/main/Env.scala +++ b/modules/round/src/main/Env.scala @@ -118,7 +118,7 @@ final class Env( roundMap = roundMap, socketHub = socketHub, messenger = messenger, - bus = system.lilaBus) + bus = system.lilaBus)(system) lazy val perfsUpdater = new PerfsUpdater(historyApi, rankingApi) diff --git a/modules/round/src/main/Socket.scala b/modules/round/src/main/Socket.scala index 4c8cb40b4c..00f1a14f97 100644 --- a/modules/round/src/main/Socket.scala +++ b/modules/round/src/main/Socket.scala @@ -18,7 +18,7 @@ import lila.hub.actorApi.round.{ IsOnGame, AnalysisAvailable } import lila.hub.actorApi.tv.{ Select => TvSelect } import lila.hub.TimeBomb import lila.socket._ -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import makeTimeout.short private[round] final class Socket( @@ -139,16 +139,13 @@ private[round] final class Socket( blackIsGone = blackIsGone) } pipeTo sender - case Join(uid, user, color, playerId, ip, userTv) => - val (enumerator, channel) = Concurrent.broadcast[JsValue] - val member = Member(channel, user, color, playerId, ip, userTv = userTv) + case AddMember(uid, member) => addMember(uid, member) notifyCrowd - playerDo(color, _.ping) - sender ! Connected(enumerator, member) + playerDo(member.color, _.ping) if (member.userTv.isDefined) refreshSubscriptions if (member.owner) lilaBus.publish( - lila.hub.actorApi.round.SocketEvent.OwnerJoin(gameId, color, ip), + lila.hub.actorApi.round.SocketEvent.OwnerJoin(gameId, member.color, member.ip), 'roundDoor) case Nil => diff --git a/modules/round/src/main/SocketHandler.scala b/modules/round/src/main/SocketHandler.scala index ff7292af94..eaf99c6fa2 100644 --- a/modules/round/src/main/SocketHandler.scala +++ b/modules/round/src/main/SocketHandler.scala @@ -14,7 +14,7 @@ import lila.common.PimpedJson._ import lila.game.{ Game, Pov, PovRef, PlayerRef, GameRepo } import lila.hub.actorApi.map._ import lila.hub.actorApi.round.Berserk -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.Handler import lila.user.User import makeTimeout.short @@ -24,7 +24,7 @@ private[round] final class SocketHandler( socketHub: ActorRef, hub: lila.hub.Env, messenger: Messenger, - bus: lila.common.Bus) { + bus: lila.common.Bus)(implicit val system: ActorSystem) { private def controller( gameId: String, @@ -100,7 +100,7 @@ private[round] final class SocketHandler( uid: String, user: Option[User], ip: String, - userTv: Option[String]): Fu[Option[JsSocketHandler]] = + userTv: Option[String]): Fu[Option[JsFlow]] = GameRepo.pov(gameId, colorName) flatMap { _ ?? { join(_, none, uid, "", user, ip, userTv = userTv) map some } } @@ -110,7 +110,7 @@ private[round] final class SocketHandler( uid: String, token: String, user: Option[User], - ip: String): Fu[JsSocketHandler] = + ip: String): Fu[JsFlow] = join(pov, Some(pov.playerId), uid, token, user, ip, userTv = none) private def join( @@ -120,21 +120,16 @@ private[round] final class SocketHandler( token: String, user: Option[User], ip: String, - userTv: Option[String]): Fu[JsSocketHandler] = { - val join = Join( - uid = uid, - user = user, - color = pov.color, - playerId = playerId, - ip = ip, - userTv = userTv) - socketHub ? Get(pov.gameId) mapTo manifest[ActorRef] flatMap { socket => - Handler(hub, socket, uid, join, user map (_.id)) { - case Connected(enum, member) => - (controller(pov.gameId, socket, uid, pov.ref, member), enum, member) + userTv: Option[String]): Fu[JsFlow] = + socketHub ? Get(pov.gameId) mapTo manifest[ActorRef] map { socket => + Handler.actorRef { out => + val member = Member(out, user, pov.color, playerId, ip, userTv = userTv) + socket ! AddMember(uid, member) + Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + controller(pov.gameId, socket, uid, pov.ref, member) + } } } - } private def parseMove(o: JsObject) = for { d ← o obj "d" @@ -142,7 +137,7 @@ private[round] final class SocketHandler( dest ← d str "to" prom = d str "promotion" move <- Uci.Move.fromStrings(orig, dest, prom) - blur = (d int "b") == Some(1) + blur = d int "b" contains 1 lag = d int "lag" } yield (move, blur, ~lag) @@ -151,7 +146,7 @@ private[round] final class SocketHandler( role ← d str "role" pos ← d str "pos" drop <- Uci.Drop.fromStrings(role, pos) - blur = (d int "b") == Some(1) + blur = d int "b" contains 1 lag = d int "lag" } yield (drop, blur, ~lag) diff --git a/modules/round/src/main/actorApi.scala b/modules/round/src/main/actorApi.scala index 431e94a5e7..d2bfe3da1a 100644 --- a/modules/round/src/main/actorApi.scala +++ b/modules/round/src/main/actorApi.scala @@ -3,6 +3,7 @@ package actorApi import scala.concurrent.duration.FiniteDuration import scala.concurrent.Promise +import akka.actor.ActorRef import chess.Color import chess.format.Uci @@ -29,7 +30,7 @@ sealed trait Member extends SocketMember { object Member { def apply( - channel: JsChannel, + out: ActorRef, user: Option[User], color: Color, playerIdOption: Option[String], @@ -37,14 +38,14 @@ object Member { userTv: Option[String]): Member = { val userId = user map (_.id) val troll = user.??(_.troll) - playerIdOption.fold[Member](Watcher(channel, userId, color, troll, ip, userTv)) { playerId => - Owner(channel, userId, playerId, color, troll, ip) + playerIdOption.fold[Member](Watcher(out, userId, color, troll, ip, userTv)) { playerId => + Owner(out, userId, playerId, color, troll, ip) } } } case class Owner( - channel: JsChannel, + out: ActorRef, userId: Option[String], playerId: String, color: Color, @@ -56,7 +57,7 @@ case class Owner( } case class Watcher( - channel: JsChannel, + out: ActorRef, userId: Option[String], color: Color, troll: Boolean, @@ -66,14 +67,8 @@ case class Watcher( val playerIdOption = none } -case class Join( - uid: String, - user: Option[User], - color: Color, - playerId: Option[String], - ip: String, - userTv: Option[String]) -case class Connected(enumerator: JsEnumerator, member: Member) +case class AddMember(uid: String, member: Member) + case class Bye(color: Color) case class IsGone(color: Color) case object GetSocketStatus diff --git a/modules/simul/src/main/Env.scala b/modules/simul/src/main/Env.scala index 7012f67ba2..e5d4c5266c 100644 --- a/modules/simul/src/main/Env.scala +++ b/modules/simul/src/main/Env.scala @@ -18,7 +18,6 @@ final class Env( scheduler: lila.common.Scheduler, db: lila.db.Env, mongoCache: lila.memo.MongoCache.Builder, - flood: lila.security.Flood, hub: lila.hub.Env, lightUser: String => Option[lila.common.LightUser], onGameStart: String => Unit, @@ -71,8 +70,7 @@ final class Env( hub = hub, socketHub = socketHub, chat = hub.actor.chat, - flood = flood, - exists = repo.exists) + exists = repo.exists)(system) system.lilaBus.subscribe(system.actorOf(Props(new Actor { import akka.pattern.pipe @@ -122,7 +120,6 @@ object Env { scheduler = lila.common.PlayApp.scheduler, db = lila.db.Env.current, mongoCache = lila.memo.Env.current.mongoCache, - flood = lila.security.Env.current.flood, hub = lila.hub.Env.current, lightUser = lila.user.Env.current.lightUser, onGameStart = lila.game.Env.current.onStart, diff --git a/modules/simul/src/main/Socket.scala b/modules/simul/src/main/Socket.scala index 95ebb592cd..71376f8053 100644 --- a/modules/simul/src/main/Socket.scala +++ b/modules/simul/src/main/Socket.scala @@ -1,7 +1,6 @@ package lila.simul import akka.actor._ -import play.api.libs.iteratee._ import play.api.libs.json._ import scala.concurrent.duration._ @@ -9,7 +8,7 @@ import actorApi._ import lila.common.LightUser import lila.hub.actorApi.round.MoveEvent import lila.hub.TimeBomb -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.{ SocketActor, History, Historical } private[simul] final class Socket( @@ -78,12 +77,9 @@ private[simul] final class Socket( case Socket.GetUserIds => sender ! userIds - case Join(uid, user) => - val (enumerator, channel) = Concurrent.broadcast[JsValue] - val member = Member(channel, user) + case AddMember(uid, member) => addMember(uid, member) notifyCrowd - sender ! Connected(enumerator, member) case Quit(uid) => quit(uid) diff --git a/modules/simul/src/main/SocketHandler.scala b/modules/simul/src/main/SocketHandler.scala index e88d0e157c..b2b29fcf0d 100644 --- a/modules/simul/src/main/SocketHandler.scala +++ b/modules/simul/src/main/SocketHandler.scala @@ -1,16 +1,12 @@ package lila.simul -import scala.concurrent.duration._ - import akka.actor._ import akka.pattern.ask import actorApi._ import lila.common.PimpedJson._ import lila.hub.actorApi.map._ -import lila.security.Flood -import akka.actor.ActorSelection -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.Handler import lila.user.User import makeTimeout.short @@ -19,35 +15,26 @@ private[simul] final class SocketHandler( hub: lila.hub.Env, socketHub: ActorRef, chat: ActorSelection, - flood: Flood, - exists: Simul.ID => Fu[Boolean]) { + exists: Simul.ID => Fu[Boolean])(implicit val system: ActorSystem) { def join( simId: String, uid: String, - user: Option[User]): Fu[Option[JsSocketHandler]] = - exists(simId) flatMap { - _ ?? { - for { - socket ← socketHub ? Get(simId) mapTo manifest[ActorRef] - join = Join(uid = uid, user = user) - handler ← Handler(hub, socket, uid, join, user map (_.id)) { - case Connected(enum, member) => - (controller(socket, simId, uid, member), enum, member) + user: Option[User]): Fu[Option[JsFlow]] = exists(simId) flatMap { + _ ?? { + socketHub ? Get(simId) mapTo manifest[ActorRef] map { socket => + Handler.actorRef { out => + val member = Member(out, user) + socket ! AddMember(uid, member) + Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } + case ("talk", o) => o str "d" foreach { text => + member.userId foreach { userId => + chat ! lila.chat.actorApi.UserTalk(simId, userId, text, socket) + } + } } - } yield handler.some - } - } - - private def controller( - socket: ActorRef, - simId: String, - uid: String, - member: Member): Handler.Controller = { - case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } - case ("talk", o) => o str "d" foreach { text => - member.userId foreach { userId => - chat ! lila.chat.actorApi.UserTalk(simId, userId, text, socket) + }.some } } } diff --git a/modules/simul/src/main/actorApi.scala b/modules/simul/src/main/actorApi.scala index f4c646c03c..aeeb4706df 100644 --- a/modules/simul/src/main/actorApi.scala +++ b/modules/simul/src/main/actorApi.scala @@ -1,34 +1,33 @@ package lila.simul package actorApi +import akka.actor.ActorRef + +import lila.game.Game import lila.socket.SocketMember import lila.user.User -import lila.game.Game private[simul] case class Member( - channel: JsChannel, + out: ActorRef, userId: Option[String], troll: Boolean) extends SocketMember private[simul] object Member { - def apply(channel: JsChannel, user: Option[User]): Member = Member( - channel = channel, + def apply(out: ActorRef, user: Option[User]): Member = Member( + out = out, userId = user map (_.id), troll = user.??(_.troll)) } +private[simul] case class AddMember(uid: String, member: Member) private[simul] case class Messadata(trollish: Boolean = false) -private[simul] case class Join( - uid: String, - user: Option[User]) private[simul] case class Talk(tourId: String, u: String, t: String, troll: Boolean) private[simul] case class StartGame(game: Game, hostId: String) private[simul] case class StartSimul(firstGame: Game, hostId: String) private[simul] case class HostIsOn(gameId: String) private[simul] case object Reload private[simul] case object Aborted -private[simul] case class Connected(enumerator: JsEnumerator, member: Member) private[simul] case object NotifyCrowd diff --git a/modules/site/src/main/SocketHandler.scala b/modules/site/src/main/SocketHandler.scala index 8d8eabe2d4..b0e02ed1ca 100644 --- a/modules/site/src/main/SocketHandler.scala +++ b/modules/site/src/main/SocketHandler.scala @@ -1,29 +1,19 @@ package lila.site -import scala.concurrent.duration._ - import akka.actor._ import play.api.libs.json._ -import play.api.libs.streams.ActorFlow import actorApi._ -import lila.common.PimpedJson._ import lila.socket._ -import lila.socket.actorApi.StartWatching private[site] final class SocketHandler( socket: ActorRef, hub: lila.hub.Env)(implicit val system: ActorSystem) { - def apply( - uid: String, - userId: Option[String], - flag: Option[String]): JsFlow = - ActorFlow.actorRef[JsValue, JsValue] { out => + def apply(uid: String, userId: Option[String], flag: Option[String]): JsFlow = + Handler.actorRef { out => val member = Member(out, userId, flag) socket ! AddMember(uid, member) - val controller = Handler.completeController(hub, socket, member, uid, userId)(Handler.emptyController) - lila.socket.SocketMemberActor.props(out, controller) + Handler.props(out)(hub, socket, member, uid, userId)(Handler.emptyController) } - } diff --git a/modules/site/src/main/actorApi.scala b/modules/site/src/main/actorApi.scala index 6008232123..1d5e29710e 100644 --- a/modules/site/src/main/actorApi.scala +++ b/modules/site/src/main/actorApi.scala @@ -6,7 +6,7 @@ import play.api.libs.json._ import lila.socket.SocketMember case class Member( - actor: akka.actor.ActorRef, + out: akka.actor.ActorRef, userId: Option[String], flag: Option[String]) extends SocketMember { @@ -14,7 +14,4 @@ case class Member( def hasFlag(f: String) = flag ?? (f ==) } - -case class Join(uid: String, userId: Option[String], flag: Option[String]) case class AddMember(uid: String, member: Member) -private[site] case class Connected(enumerator: JsEnumerator, member: Member) diff --git a/modules/socket/src/main/Channel.scala b/modules/socket/src/main/Channel.scala index 67c52c7708..af059679dd 100644 --- a/modules/socket/src/main/Channel.scala +++ b/modules/socket/src/main/Channel.scala @@ -2,7 +2,7 @@ package lila.socket import actorApi.SocketLeave import akka.actor._ -import play.api.libs.json.JsValue +import play.api.libs.json.JsObject final class Channel extends Actor { @@ -36,5 +36,5 @@ object Channel { case class Sub(member: SocketMember) case class UnSub(member: SocketMember) - case class Publish(msg: JsValue) + case class Publish(msg: JsObject) } diff --git a/modules/socket/src/main/Handler.scala b/modules/socket/src/main/Handler.scala index bc6ebfbe77..4326de4333 100644 --- a/modules/socket/src/main/Handler.scala +++ b/modules/socket/src/main/Handler.scala @@ -1,12 +1,11 @@ package lila.socket -import akka.actor.ActorRef -import akka.NotUsed +import akka.actor.{ ActorRef, Props, ActorSystem } import akka.pattern.{ ask, pipe } import akka.stream._ import akka.stream.scaladsl._ -import play.api.libs.iteratee.{ Iteratee, Enumerator } import play.api.libs.json._ +import play.api.libs.streams.ActorFlow import scala.concurrent.duration._ import actorApi._ @@ -14,30 +13,30 @@ import lila.common.PimpedJson._ import lila.hub.actorApi.relation.ReloadOnlineFriends import lila.socket.Socket.makeMessage import makeTimeout.large -import Step.openingWriter object Handler { type Controller = PartialFunction[(String, JsObject), Unit] - type Connecter = PartialFunction[Any, (Controller, JsEnumerator, SocketMember)] val emptyController: Controller = PartialFunction.empty lazy val AnaRateLimit = new lila.memo.RateLimit(90, 60 seconds, "socket analysis move") - def completeController( + def actorRef(withOut: ActorRef => Props)(implicit system: ActorSystem): JsFlow = + ActorFlow.actorRef[JsObject, JsObject](withOut) + + def props(out: ActorRef)( hub: lila.hub.Env, socket: ActorRef, member: SocketMember, uid: String, - userId: Option[String])(controller: Controller): JsValue => Unit = { + userId: Option[String])(controller: Controller): Props = { val control = controller orElse baseController(hub, socket, member, uid, userId) - in => - in.asOpt[JsObject] foreach { obj => - obj str "t" foreach { t => - control.lift(t -> obj) - } + lila.socket.SocketMemberActor.props(out, in => + in str "t" foreach { t => + control.lift(t -> in) } + ) } def baseController( @@ -83,6 +82,7 @@ object Handler { } } case ("anaDests", o) => AnaRateLimit(uid) { + import Step.openingWriter AnaDests parse o match { case Some(req) => member push makeMessage("dests", Json.obj( @@ -97,27 +97,4 @@ object Handler { } case _ => // logwarn("Unhandled msg: " + msg) } - - def apply( - hub: lila.hub.Env, - socket: ActorRef, - uid: String, - join: Any, - userId: Option[String])(connecter: Connecter): Fu[JsSocketHandler] = { - - def iteratee(controller: Controller, member: SocketMember): JsIteratee = { - val control = controller orElse baseController(hub, socket, member, uid, userId) - Iteratee.foreach[JsValue](jsv => - jsv.asOpt[JsObject] foreach { obj => - obj str "t" foreach { t => - control.lift(t -> obj) - } - } - ).map(_ => socket ! Quit(uid)) - } - - socket ? join map connecter map { - case (controller, enum, member) => iteratee(controller, member) -> enum - } - } } diff --git a/modules/socket/src/main/SocketMember.scala b/modules/socket/src/main/SocketMember.scala index 4f1319eaf8..b15d73c439 100644 --- a/modules/socket/src/main/SocketMember.scala +++ b/modules/socket/src/main/SocketMember.scala @@ -1,11 +1,11 @@ package lila.socket import akka.actor.{ ActorRef, PoisonPill } -import play.api.libs.json.JsValue +import play.api.libs.json.JsObject trait SocketMember extends Ordered[SocketMember] { - protected val actor: ActorRef + protected val out: ActorRef val userId: Option[String] val troll: Boolean @@ -13,17 +13,17 @@ trait SocketMember extends Ordered[SocketMember] { def compare(other: SocketMember) = ~userId compare ~other.userId - def push(msg: JsValue) = actor ! msg + def push(msg: JsObject) = out ! msg - def end = actor ! PoisonPill + def end = out ! PoisonPill } object SocketMember { - def apply(a: ActorRef): SocketMember = apply(a, none, false) + def apply(o: ActorRef): SocketMember = apply(o, none, false) - def apply(a: ActorRef, uid: Option[String], tr: Boolean): SocketMember = new SocketMember { - val actor = a + def apply(o: ActorRef, uid: Option[String], tr: Boolean): SocketMember = new SocketMember { + val out = o val userId = uid val troll = tr } diff --git a/modules/socket/src/main/SocketMemberActor.scala b/modules/socket/src/main/SocketMemberActor.scala index c47fb53e7e..313d69f399 100644 --- a/modules/socket/src/main/SocketMemberActor.scala +++ b/modules/socket/src/main/SocketMemberActor.scala @@ -1,12 +1,12 @@ package lila.socket import akka.actor._ -import play.api.libs.json.JsValue +import play.api.libs.json.JsObject // implements ActorFlow.actorRef out actor final class SocketMemberActor( out: ActorRef, - handler: JsValue => Unit) extends Actor { + handler: JsObject => Unit) extends Actor { import SocketMemberActor._ @@ -14,14 +14,14 @@ final class SocketMemberActor( // case Send(msg) => out ! msg - case msg: JsValue => handler(msg) + case msg: JsObject => handler(msg) } } object SocketMemberActor { - // case class Send(msg: JsValue) + // case class Send(msg: JsObject) - def props(out: ActorRef, handler: JsValue => Unit) = + def props(out: ActorRef, handler: JsObject => Unit) = Props(new SocketMemberActor(out, handler)) } diff --git a/modules/socket/src/main/WithSocket.scala b/modules/socket/src/main/WithSocket.scala index a62fb4aefa..a3fa4d177f 100644 --- a/modules/socket/src/main/WithSocket.scala +++ b/modules/socket/src/main/WithSocket.scala @@ -1,21 +1,10 @@ package lila.socket -import ornicar.scalalib.Zero -import play.api.libs.iteratee.{ Iteratee, Enumerator } -import play.api.libs.json._ - -import akka.NotUsed -import akka.stream._ -import akka.stream.scaladsl._ +import akka.stream.scaladsl.Flow +import play.api.libs.json.JsObject +import play.api.libs.streams.ActorFlow trait WithSocket { - type JsChannel = play.api.libs.iteratee.Concurrent.Channel[JsValue] - type JsEnumerator = Enumerator[JsValue] - type JsIteratee = Iteratee[JsValue, _] - type JsSocketHandler = (JsIteratee, JsEnumerator) - - type JsFlow = Flow[JsValue, JsValue, _] - // type JsSource = Source[JsValue, NotUsed] - // type JsSink = Sink[JsValue, NotUsed] + type JsFlow = Flow[JsObject, JsObject, _] } diff --git a/modules/socket/src/main/actorApi.scala b/modules/socket/src/main/actorApi.scala index 741740805d..6c011a2410 100644 --- a/modules/socket/src/main/actorApi.scala +++ b/modules/socket/src/main/actorApi.scala @@ -2,11 +2,7 @@ package lila.socket package actorApi import play.api.libs.json.JsObject -import akka.actor.ActorRef -case class Connected[M <: SocketMember]( - enumerator: JsEnumerator, - member: M) case class Sync(uid: String, friends: List[String]) case class Ping(uid: String) case class PingVersion(uid: String, version: Int) diff --git a/modules/tournament/src/main/Env.scala b/modules/tournament/src/main/Env.scala index 9f0c879062..282236e26d 100644 --- a/modules/tournament/src/main/Env.scala +++ b/modules/tournament/src/main/Env.scala @@ -17,7 +17,6 @@ final class Env( system: ActorSystem, db: lila.db.Env, mongoCache: lila.memo.MongoCache.Builder, - flood: lila.security.Flood, hub: lila.hub.Env, roundMap: ActorRef, roundSocketHub: ActorSelection, @@ -80,8 +79,7 @@ final class Env( lazy val socketHandler = new SocketHandler( hub = hub, socketHub = socketHub, - chat = hub.actor.chat, - flood = flood) + chat = hub.actor.chat)(system) lazy val winners = new Winners( mongoCache = mongoCache, @@ -171,7 +169,6 @@ object Env { system = lila.common.PlayApp.system, db = lila.db.Env.current, mongoCache = lila.memo.Env.current.mongoCache, - flood = lila.security.Env.current.flood, hub = lila.hub.Env.current, roundMap = lila.round.Env.current.roundMap, roundSocketHub = lila.hub.Env.current.socket.round, diff --git a/modules/tournament/src/main/Socket.scala b/modules/tournament/src/main/Socket.scala index 94d0240e9e..782966e25f 100644 --- a/modules/tournament/src/main/Socket.scala +++ b/modules/tournament/src/main/Socket.scala @@ -2,7 +2,6 @@ package lila.tournament import akka.actor._ import akka.pattern.pipe -import play.api.libs.iteratee._ import play.api.libs.json._ import scala.concurrent.duration._ @@ -11,7 +10,7 @@ import lila.common.LightUser import lila.hub.actorApi.WithUserIds import lila.hub.TimeBomb import lila.memo.ExpireSetMemo -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.{ SocketActor, History, Historical } private[tournament] final class Socket( @@ -78,12 +77,9 @@ private[tournament] final class Socket( case GetVersion => sender ! history.version - case Join(uid, user) => - val (enumerator, channel) = Concurrent.broadcast[JsValue] - val member = Member(channel, user) + case AddMember(uid, member) => addMember(uid, member) notifyCrowd - sender ! Connected(enumerator, member) case Quit(uid) => quit(uid) diff --git a/modules/tournament/src/main/SocketHandler.scala b/modules/tournament/src/main/SocketHandler.scala index c1eb7f1ba7..dcc585c061 100644 --- a/modules/tournament/src/main/SocketHandler.scala +++ b/modules/tournament/src/main/SocketHandler.scala @@ -1,16 +1,12 @@ package lila.tournament -import scala.concurrent.duration._ - import akka.actor._ import akka.pattern.ask import actorApi._ import lila.common.PimpedJson._ import lila.hub.actorApi.map._ -import lila.security.Flood -import akka.actor.ActorSelection -import lila.socket.actorApi.{ Connected => _, _ } +import lila.socket.actorApi._ import lila.socket.Handler import lila.user.User import makeTimeout.short @@ -18,36 +14,25 @@ import makeTimeout.short private[tournament] final class SocketHandler( hub: lila.hub.Env, socketHub: ActorRef, - chat: ActorSelection, - flood: Flood) { + chat: ActorSelection)(implicit system: ActorSystem) { - def join( - tourId: String, - uid: String, - user: Option[User]): Fu[Option[JsSocketHandler]] = + def join(tourId: String, uid: String, user: Option[User]): Fu[Option[JsFlow]] = TournamentRepo.exists(tourId) flatMap { _ ?? { - for { - socket ← socketHub ? Get(tourId) mapTo manifest[ActorRef] - join = Join(uid = uid, user = user) - handler ← Handler(hub, socket, uid, join, user map (_.id)) { - case Connected(enum, member) => - (controller(socket, tourId, uid, member), enum, member) - } - } yield handler.some - } - } - - private def controller( - socket: ActorRef, - tourId: String, - uid: String, - member: Member): Handler.Controller = { - case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } - case ("talk", o) => o str "d" foreach { text => - member.userId foreach { userId => - chat ! lila.chat.actorApi.UserTalk(tourId, userId, text, socket) + socketHub ? Get(tourId) mapTo manifest[ActorRef] map { socket => + Handler.actorRef { out => + val member = Member(out, user) + socket ! AddMember(uid, member) + Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } + case ("talk", o) => o str "d" foreach { text => + member.userId foreach { userId => + chat ! lila.chat.actorApi.UserTalk(tourId, userId, text, socket) + } + } + } + }.some + } } } - } } diff --git a/modules/tournament/src/main/actorApi.scala b/modules/tournament/src/main/actorApi.scala index acc8fe5a03..0b0fd1bc9e 100644 --- a/modules/tournament/src/main/actorApi.scala +++ b/modules/tournament/src/main/actorApi.scala @@ -1,31 +1,30 @@ package lila.tournament package actorApi +import akka.actor.ActorRef + import lila.game.Game import lila.socket.SocketMember import lila.user.User private[tournament] case class Member( - channel: JsChannel, + out: ActorRef, userId: Option[String], troll: Boolean) extends SocketMember private[tournament] object Member { - def apply(channel: JsChannel, user: Option[User]): Member = Member( - channel = channel, + def apply(out: ActorRef, user: Option[User]): Member = Member( + out = out, userId = user map (_.id), troll = user.??(_.troll)) } private[tournament] case class Messadata(trollish: Boolean = false) -private[tournament] case class Join( - uid: String, - user: Option[User]) +private[tournament] case class AddMember(uid: String, member: Member) private[tournament] case class Talk(tourId: String, u: String, t: String, troll: Boolean) private[tournament] case object Reload private[tournament] case class StartGame(game: Game) -private[tournament] case class Connected(enumerator: JsEnumerator, member: Member) case class RemindTournament(tour: Tournament, activeUserIds: List[String]) case class TournamentTable(tours: List[Tournament]) From 118bac6f5b43f4108ffbc45d8dec8d61b22a6b25 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 12:45:06 +0700 Subject: [PATCH 21/26] implement WS rate limits with akka throttle flow --- app/controllers/LilaController.scala | 33 ----------------- app/controllers/LilaSocket.scala | 54 ++++++++++++---------------- app/controllers/Lobby.scala | 14 ++++++-- app/controllers/Main.scala | 2 +- 4 files changed, 36 insertions(+), 67 deletions(-) diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 428b68263a..22f318dbf4 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -6,7 +6,6 @@ import play.api.data.Form import play.api.http._ import play.api.libs.json.{ Json, JsValue, JsObject, JsArray, Writes } import play.api.mvc._, Results._ -import play.api.mvc.WebSocket.MessageFlowTransformer import play.twirl.api.Html import lila.api.{ PageData, Context, HeaderContext, BodyContext, TokenBucket } @@ -47,30 +46,6 @@ private[controllers] trait LilaController CACHE_CONTROL -> "no-cache, no-store, must-revalidate", EXPIRES -> "0" ) - private implicit val jsonMessageFlowTransformer: MessageFlowTransformer[JsObject, JsObject] = { - import scala.util.control.NonFatal - import play.api.libs.streams.AkkaStreams - import websocket._ - def closeOnException[T](block: => Option[T]) = try { - Left(block getOrElse { - sys error "Not a JsObject" - }) - } - catch { - case NonFatal(e) => Right(CloseMessage(Some(CloseCodes.Unacceptable), - "Unable to parse json message")) - } - - new MessageFlowTransformer[JsObject, JsObject] { - def transform(flow: Flow[JsObject, JsObject, _]) = { - AkkaStreams.bypassWith[Message, JsObject, Message](Flow[Message].collect { - case BinaryMessage(data) => closeOnException(Json.parse(data.iterator.asInputStream).asOpt[JsObject]) - case TextMessage(text) => closeOnException(Json.parse(text).asOpt[JsObject]) - })(flow map { json => TextMessage(Json.stringify(json)) }) - } - } - } - protected def Socket(f: Context => Fu[JsFlow]) = WebSocket.acceptOrResult[JsObject, JsObject] { req => reqToCtx(req) flatMap f map Right.apply @@ -81,14 +56,6 @@ private[controllers] trait LilaController reqToCtx(req) flatMap f } - protected def SocketOptionLimited(consumer: TokenBucket.Consumer, name: String)(f: Context => Fu[Option[JsFlow]]) = - rateLimitedSocket[A](consumer, name) { ctx => - f(ctx) map { - case None => Left(NotFound(jsonError("socket resource not found"))) - case Some(flow) => Right(flow) - } - } - protected def SocketOption(f: Context => Fu[Option[JsFlow]]): WebSocket = WebSocket.acceptOrResult[JsObject, JsObject] { req => reqToCtx(req) flatMap f map { diff --git a/app/controllers/LilaSocket.scala b/app/controllers/LilaSocket.scala index 88dea834a7..5d4fce0f0d 100644 --- a/app/controllers/LilaSocket.scala +++ b/app/controllers/LilaSocket.scala @@ -1,10 +1,11 @@ package controllers +import akka.stream.scaladsl._ import play.api.http._ -import play.api.libs.iteratee._ import play.api.libs.json._ import play.api.mvc._, Results._ -import play.api.mvc.WebSocket.FrameFormatter +import play.api.mvc.WebSocket.MessageFlowTransformer +import scala.concurrent.duration._ import lila.api.{ Context, TokenBucket } import lila.app._ @@ -12,36 +13,27 @@ import lila.common.HTTPRequest trait LilaSocket { self: LilaController => - private type AcceptType[A] = Context => Fu[Either[Result, JsFlow]] - - private val logger = lila.log("ratelimit") + protected implicit val jsonMessageFlowTransformer: MessageFlowTransformer[JsObject, JsObject] = { + import scala.util.control.NonFatal + import play.api.libs.streams.AkkaStreams + import websocket._ + def closeOnException[T](block: => Option[T]) = try { + Left(block getOrElse { + sys error "Not a JsObject" + }) + } + catch { + case NonFatal(e) => Right(CloseMessage(Some(CloseCodes.Unacceptable), + "Unable to parse json message")) + } - def rateLimitedSocket(consumer: TokenBucket.Consumer, name: String)(f: AcceptType[A]): WebSocket = - WebSocket.acceptOrResult[A, A] { req => - reqToCtx(req) flatMap { ctx => - val ip = HTTPRequest lastRemoteAddress req - def userInfo = { - val sri = get("sri", req) | "none" - val username = ctx.usernameOrAnon - s"user:$username sri:$sri" - } - // logger.debug(s"socket:$name socket connect $ip $userInfo") - f(ctx).map { resultOrSocket => - resultOrSocket.right.map { - case (readIn, writeOut) => { - val limitedIn = Enumeratee.mapInputM[A] { in => - consumer(ip).map { credit => - if (credit >= 0) in - else { - logger.info(s"socket:$name socket close $ip $userInfo $in") - Input.EOF - } - } - } &> readIn - (limitedIn, writeOut) - } - } - } + new MessageFlowTransformer[JsObject, JsObject] { + def transform(flow: Flow[JsObject, JsObject, _]) = { + AkkaStreams.bypassWith[Message, JsObject, Message](Flow[Message].collect { + case BinaryMessage(data) => closeOnException(Json.parse(data.iterator.asInputStream).asOpt[JsObject]) + case TextMessage(text) => closeOnException(Json.parse(text).asOpt[JsObject]) + })(flow map { json => TextMessage(Json.stringify(json)) }) } } + } } diff --git a/app/controllers/Lobby.scala b/app/controllers/Lobby.scala index ca844a3a2f..8e9e6dc0f9 100644 --- a/app/controllers/Lobby.scala +++ b/app/controllers/Lobby.scala @@ -48,12 +48,22 @@ object Lobby extends LilaController { size = 10, rate = 6) - def socket(apiVersion: Int) = SocketOptionLimited(socketConsumer, "lobby") { implicit ctx => + import akka.stream.scaladsl.Flow + import scala.concurrent.duration._ + private val wsThrottler = Flow[JsObject].throttle( + elements = 10, + per = 5.second, + maximumBurst = 0, + mode = akka.stream.ThrottleMode.Enforcing) + + def socket(apiVersion: Int) = SocketOption { implicit ctx => get("sri") ?? { uid => Env.lobby.socketHandler( uid = uid, user = ctx.me, - mobile = getBool("mobile")) map some + mobile = getBool("mobile")) map { flow => + wsThrottler.via(flow).some + } } } diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala index ec4bd90614..a702d58377 100644 --- a/app/controllers/Main.scala +++ b/app/controllers/Main.scala @@ -37,7 +37,7 @@ object Main extends LilaController { def websocket = SocketOption { implicit ctx => get("sri") ?? { uid => - fuccess(Env.site.socketHandler(uid, ctx.userId, get("flag"))) + fuccess(Env.site.socketHandler(uid, ctx.userId, get("flag")).some) } } From 2651eb93264e8793f2ef966c1a45b9abfa6290f2 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 12:47:56 +0700 Subject: [PATCH 22/26] fix base.conf --- conf/base.conf | 4 ---- 1 file changed, 4 deletions(-) diff --git a/conf/base.conf b/conf/base.conf index 5c2fa81365..6f3c1b4c60 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -63,10 +63,6 @@ play { idleConnectionInPoolTimeout = 1 minute } } - } - crypto { - secret="CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^" - } akka.actor-system = "lila" } app { From 9fe1571fb05a3cc39e5b793c65b8d58a8d6fe014 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 14:05:23 +0700 Subject: [PATCH 23/26] simplify socket flow creation --- modules/challenge/src/main/SocketHandler.scala | 2 +- modules/lobby/src/main/SocketHandler.scala | 2 +- modules/round/src/main/SocketHandler.scala | 2 +- modules/simul/src/main/SocketHandler.scala | 2 +- modules/site/src/main/SocketHandler.scala | 2 +- modules/socket/src/main/Handler.scala | 4 ++-- modules/socket/src/main/SocketMemberActor.scala | 13 ++++--------- modules/tournament/src/main/SocketHandler.scala | 2 +- 8 files changed, 12 insertions(+), 17 deletions(-) diff --git a/modules/challenge/src/main/SocketHandler.scala b/modules/challenge/src/main/SocketHandler.scala index 031d4b4102..2e20fc17c2 100644 --- a/modules/challenge/src/main/SocketHandler.scala +++ b/modules/challenge/src/main/SocketHandler.scala @@ -24,7 +24,7 @@ private[challenge] final class SocketHandler( Handler.actorRef { out => val member = Socket.Member(out, userId, owner) socket ! Socket.AddMember(uid, member) - Handler.props(out)(hub, socket, member, uid, userId) { + Handler.props(hub, socket, member, uid, userId) { case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } diff --git a/modules/lobby/src/main/SocketHandler.scala b/modules/lobby/src/main/SocketHandler.scala index 93325388c3..edf3109d8f 100644 --- a/modules/lobby/src/main/SocketHandler.scala +++ b/modules/lobby/src/main/SocketHandler.scala @@ -44,7 +44,7 @@ private[lobby] final class SocketHandler( Handler.actorRef { out => val member = Member(out, user, blockedUserIds, uid, mobile) socket ! AddMember(uid, member) - Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + Handler.props(hub, socket, member, uid, user.map(_.id)) { controller(socket, uid, member) } } diff --git a/modules/round/src/main/SocketHandler.scala b/modules/round/src/main/SocketHandler.scala index eaf99c6fa2..4c3519cd5d 100644 --- a/modules/round/src/main/SocketHandler.scala +++ b/modules/round/src/main/SocketHandler.scala @@ -125,7 +125,7 @@ private[round] final class SocketHandler( Handler.actorRef { out => val member = Member(out, user, pov.color, playerId, ip, userTv = userTv) socket ! AddMember(uid, member) - Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + Handler.props(hub, socket, member, uid, user.map(_.id)) { controller(pov.gameId, socket, uid, pov.ref, member) } } diff --git a/modules/simul/src/main/SocketHandler.scala b/modules/simul/src/main/SocketHandler.scala index b2b29fcf0d..b3dc5455d1 100644 --- a/modules/simul/src/main/SocketHandler.scala +++ b/modules/simul/src/main/SocketHandler.scala @@ -26,7 +26,7 @@ private[simul] final class SocketHandler( Handler.actorRef { out => val member = Member(out, user) socket ! AddMember(uid, member) - Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + Handler.props(hub, socket, member, uid, user.map(_.id)) { case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } case ("talk", o) => o str "d" foreach { text => member.userId foreach { userId => diff --git a/modules/site/src/main/SocketHandler.scala b/modules/site/src/main/SocketHandler.scala index b0e02ed1ca..40dd23b241 100644 --- a/modules/site/src/main/SocketHandler.scala +++ b/modules/site/src/main/SocketHandler.scala @@ -14,6 +14,6 @@ private[site] final class SocketHandler( Handler.actorRef { out => val member = Member(out, userId, flag) socket ! AddMember(uid, member) - Handler.props(out)(hub, socket, member, uid, userId)(Handler.emptyController) + Handler.props(hub, socket, member, uid, userId)(Handler.emptyController) } } diff --git a/modules/socket/src/main/Handler.scala b/modules/socket/src/main/Handler.scala index 4326de4333..7bd8cc615b 100644 --- a/modules/socket/src/main/Handler.scala +++ b/modules/socket/src/main/Handler.scala @@ -25,14 +25,14 @@ object Handler { def actorRef(withOut: ActorRef => Props)(implicit system: ActorSystem): JsFlow = ActorFlow.actorRef[JsObject, JsObject](withOut) - def props(out: ActorRef)( + def props( hub: lila.hub.Env, socket: ActorRef, member: SocketMember, uid: String, userId: Option[String])(controller: Controller): Props = { val control = controller orElse baseController(hub, socket, member, uid, userId) - lila.socket.SocketMemberActor.props(out, in => + lila.socket.SocketMemberActor.props(in => in str "t" foreach { t => control.lift(t -> in) } diff --git a/modules/socket/src/main/SocketMemberActor.scala b/modules/socket/src/main/SocketMemberActor.scala index 313d69f399..35ba55fe6a 100644 --- a/modules/socket/src/main/SocketMemberActor.scala +++ b/modules/socket/src/main/SocketMemberActor.scala @@ -5,23 +5,18 @@ import play.api.libs.json.JsObject // implements ActorFlow.actorRef out actor final class SocketMemberActor( - out: ActorRef, handler: JsObject => Unit) extends Actor { - import SocketMemberActor._ - def receive = { - // case Send(msg) => out ! msg - case msg: JsObject => handler(msg) + + case m => lila.log("socket").warn(s"SocketMemberActor received $m") } } object SocketMemberActor { - // case class Send(msg: JsObject) - - def props(out: ActorRef, handler: JsObject => Unit) = - Props(new SocketMemberActor(out, handler)) + def props(handler: JsObject => Unit) = + Props(new SocketMemberActor(handler)) } diff --git a/modules/tournament/src/main/SocketHandler.scala b/modules/tournament/src/main/SocketHandler.scala index dcc585c061..6e8c1a2e66 100644 --- a/modules/tournament/src/main/SocketHandler.scala +++ b/modules/tournament/src/main/SocketHandler.scala @@ -23,7 +23,7 @@ private[tournament] final class SocketHandler( Handler.actorRef { out => val member = Member(out, user) socket ! AddMember(uid, member) - Handler.props(out)(hub, socket, member, uid, user.map(_.id)) { + Handler.props(hub, socket, member, uid, user.map(_.id)) { case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) } case ("talk", o) => o str "d" foreach { text => member.userId foreach { userId => From 553ce536a06d4a6b2d17c7ecada85b98da15af41 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 15:30:40 +0700 Subject: [PATCH 24/26] fix lobby burst --- app/controllers/Lobby.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/Lobby.scala b/app/controllers/Lobby.scala index 8e9e6dc0f9..fdcad1c7e9 100644 --- a/app/controllers/Lobby.scala +++ b/app/controllers/Lobby.scala @@ -53,7 +53,7 @@ object Lobby extends LilaController { private val wsThrottler = Flow[JsObject].throttle( elements = 10, per = 5.second, - maximumBurst = 0, + maximumBurst = 10, mode = akka.stream.ThrottleMode.Enforcing) def socket(apiVersion: Int) = SocketOption { implicit ctx => From 6f1dc19e8a484b0dcdc5298cd31c34c1778983f6 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Apr 2016 17:27:01 +0700 Subject: [PATCH 25/26] rewrite event sources WIP --- app/controllers/WorldMap.scala | 11 +++-- conf/base.conf | 1 + modules/common/src/main/AkkaStream.scala | 23 +++++++++++ modules/common/src/main/PackageObject.scala | 3 +- modules/round/src/main/TvBroadcast.scala | 16 ++++--- modules/worldMap/src/main/Env.scala | 10 +---- modules/worldMap/src/main/Stream.scala | 46 +++++++++++++-------- 7 files changed, 70 insertions(+), 40 deletions(-) create mode 100644 modules/common/src/main/AkkaStream.scala diff --git a/app/controllers/WorldMap.scala b/app/controllers/WorldMap.scala index 9ffbde72e3..e4b1940d2b 100644 --- a/app/controllers/WorldMap.scala +++ b/app/controllers/WorldMap.scala @@ -14,10 +14,13 @@ object WorldMap extends LilaController { } def stream = Action.async { - Env.worldMap.getStream map { stream => - Ok.chunked( - stream &> EventSource() - ) as "text/event-stream" + import akka.pattern.ask + import makeTimeout.short + import lila.worldMap.Stream + import akka.stream.scaladsl.Source + Env.worldMap.stream ? Stream.GetPublisher mapTo manifest[Stream.PublisherType] map { publisher => + val source = Source fromPublisher publisher + Ok.chunked(source via EventSource.flow).as("text/event-stream") } } } diff --git a/conf/base.conf b/conf/base.conf index 6f3c1b4c60..a99ddb3997 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -64,6 +64,7 @@ play { } } akka.actor-system = "lila" + crypto.secret="CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^" } app { scheduler { diff --git a/modules/common/src/main/AkkaStream.scala b/modules/common/src/main/AkkaStream.scala new file mode 100644 index 0000000000..819dcea541 --- /dev/null +++ b/modules/common/src/main/AkkaStream.scala @@ -0,0 +1,23 @@ +package lila.common + +import akka.actor._ +import akka.stream._ +import akka.stream.scaladsl._ +import org.reactivestreams.Publisher + +object AkkaStream { + + def actorPublisher[T]( + bufferSize: Int, + overflowStrategy: OverflowStrategy)(implicit materializer: Materializer): (ActorRef, Publisher[T]) = { + Source.actorRef[T](20, OverflowStrategy.dropHead) + .toMat(Sink asPublisher false)(Keep.both).run() + } + + def actorSource[T]( + bufferSize: Int, + overflowStrategy: OverflowStrategy)(implicit materializer: Materializer): (ActorRef, Source[T, _]) = { + val (out, publisher) = actorPublisher(bufferSize, overflowStrategy) + (out, Source fromPublisher publisher) + } +} diff --git a/modules/common/src/main/PackageObject.scala b/modules/common/src/main/PackageObject.scala index 1ae1ad0a30..e7a3e993ee 100644 --- a/modules/common/src/main/PackageObject.scala +++ b/modules/common/src/main/PackageObject.scala @@ -97,7 +97,8 @@ trait WithPlay { self: PackageObject => implicit def execontext = play.api.libs.concurrent.Execution.defaultContext - implicit def materializer(implicit system: akka.actor.ActorSystem) = akka.stream.ActorMaterializer() + implicit def materializer(implicit system: akka.actor.ActorSystem): akka.stream.Materializer = + akka.stream.ActorMaterializer() implicit val LilaFutureMonad = new Monad[Fu] { override def map[A, B](fa: Fu[A])(f: A => B) = fa map f diff --git a/modules/round/src/main/TvBroadcast.scala b/modules/round/src/main/TvBroadcast.scala index c194adbce3..2a6d8e50ad 100644 --- a/modules/round/src/main/TvBroadcast.scala +++ b/modules/round/src/main/TvBroadcast.scala @@ -10,23 +10,21 @@ import play.api.libs.json._ private final class TvBroadcast extends Actor { - implicit val mat = materializer(context.system) - - val (actorRef, publisher) = Source.actorRef(20, OverflowStrategy.dropHead) - .toMat(Sink asPublisher false)(Keep.both).run() + val (out, publisher) = + lila.common.AkkaStream.actorPublisher(20, OverflowStrategy.dropHead)(materializer(context.system)) private var featuredId = none[String] def receive = { - case TvBroadcast.GetPublisher => sender ! publisher + case TvBroadcast.GetPublisher => sender ! publisher case ChangeFeatured(id, msg) => featuredId = id.some - actorRef ! msg + out ! msg case move: MoveEvent if Some(move.gameId) == featuredId => - actorRef ! makeMessage("fen", Json.obj( + out ! makeMessage("fen", Json.obj( "fen" -> move.fen, "lm" -> move.move )) @@ -35,7 +33,7 @@ private final class TvBroadcast extends Actor { object TvBroadcast { - import org.reactivestreams.Publisher - type PublisherType = Publisher[JsValue] + type PublisherType = org.reactivestreams.Publisher[JsValue] + case object GetPublisher } diff --git a/modules/worldMap/src/main/Env.scala b/modules/worldMap/src/main/Env.scala index c278eb5627..b0b35e22f3 100644 --- a/modules/worldMap/src/main/Env.scala +++ b/modules/worldMap/src/main/Env.scala @@ -13,19 +13,11 @@ final class Env( private val GeoIPFile = config getString "geoip.file" private val GeoIPCacheTtl = config duration "geoip.cache_ttl" - private val stream = system.actorOf( + val stream = system.actorOf( Props(new Stream( geoIp = MaxMindIpGeo(GeoIPFile, 0), geoIpCacheTtl = GeoIPCacheTtl))) system.lilaBus.subscribe(stream, 'changeFeaturedGame, 'streams, 'nbMembers, 'nbRounds) - - def getStream = { - import play.api.libs.iteratee._ - import play.api.libs.json._ - import akka.pattern.ask - import makeTimeout.short - stream ? Stream.Get mapTo manifest[Enumerator[JsValue]] - } } object Env { diff --git a/modules/worldMap/src/main/Stream.scala b/modules/worldMap/src/main/Stream.scala index fcdb91c520..8970106704 100644 --- a/modules/worldMap/src/main/Stream.scala +++ b/modules/worldMap/src/main/Stream.scala @@ -1,10 +1,11 @@ package lila.worldMap import akka.actor._ +import akka.stream._ +import akka.stream.scaladsl._ import com.sanoma.cda.geoip.{ MaxMindIpGeo, IpLocation } import java.security.MessageDigest import lila.hub.actorApi.round.SocketEvent -import play.api.libs.iteratee._ import play.api.libs.json._ import scala.concurrent.duration._ @@ -18,11 +19,25 @@ private final class Stream( val games = scala.collection.mutable.Map.empty[String, Stream.Game] - private def makeMd5 = MessageDigest getInstance "MD5" + val (out, realtime) = + lila.common.AkkaStream.actorSource(20, OverflowStrategy.dropHead)(materializer(context.system)) - private val loadCompleteJson = Json.obj("loadComplete" -> true) + def preload = Source[JsValue](games.values.map(game2json(makeMd5)).toList) + val preloadComplete = Source[JsValue](List(Json.obj("loadComplete" -> true): JsValue)) + + val transformer = Flow.fromFunction[Stream.Event, JsValue] { + case Stream.Event.Add(game) => game2json(makeMd5)(game) + case Stream.Event.Remove(id) => Json.obj("id" -> id) + } + + def makeSource = + Source.combine(preload, preloadComplete, realtime via transformer)(_ => Concat()) + + def makePublisher = + makeSource.toMat(Sink asPublisher false)(Keep.both).run() def receive = { + case SocketEvent.OwnerJoin(id, color, ip) => ipCache get ip foreach { point => val game = games get id match { @@ -30,25 +45,19 @@ private final class Stream( case None => Stream.Game(id, List(point)) } games += (id -> game) - channel push Stream.Event.Add(game) + out ! Stream.Event.Add(game) } + case SocketEvent.Stop(id) => games -= id - channel push Stream.Event.Remove(id) - case Stream.Get => sender ! { - Enumerator.enumerate(games.values.map(game2json(makeMd5))) andThen - Enumerator.enumerate(List(loadCompleteJson)) andThen - producer - } - } + out ! Stream.Event.Remove(id) - val (enumerator, channel) = Concurrent.broadcast[Stream.Event] - - val producer = enumerator &> Enumeratee.map[Stream.Event].apply[JsValue] { - case Stream.Event.Add(game) => game2json(makeMd5)(game) - case Stream.Event.Remove(id) => Json.obj("id" -> id) + case Stream.GetPublisher => sender ! makePublisher } + implicit val mat = materializer(context.system) + + def makeMd5 = MessageDigest getInstance "MD5" val ipCache = lila.memo.Builder.cache(geoIpCacheTtl, ipToPoint) def ipToPoint(ip: String): Option[Stream.Point] = geoIp getLocation ip flatMap Stream.toPoint @@ -56,7 +65,10 @@ private final class Stream( object Stream { - case object Get + case object GetPublisher + + import org.reactivestreams.Publisher + type PublisherType = Publisher[JsValue] case class Game(id: String, points: List[Point]) { From 4975f6c9ca1a90b134abbbc94130c04dc196a5a3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 6 Apr 2016 09:48:34 +0700 Subject: [PATCH 26/26] more fighting with akka streams --- app/controllers/WorldMap.scala | 7 +--- app/views/site/worldMap.scala.html | 1 - modules/common/src/main/AkkaStream.scala | 2 +- modules/common/src/main/PackageObject.scala | 2 + modules/worldMap/src/main/Env.scala | 11 ++++- modules/worldMap/src/main/Stream.scala | 46 ++++++++++----------- public/worldMap/app.js | 2 +- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/controllers/WorldMap.scala b/app/controllers/WorldMap.scala index e4b1940d2b..3e233cd162 100644 --- a/app/controllers/WorldMap.scala +++ b/app/controllers/WorldMap.scala @@ -14,12 +14,7 @@ object WorldMap extends LilaController { } def stream = Action.async { - import akka.pattern.ask - import makeTimeout.short - import lila.worldMap.Stream - import akka.stream.scaladsl.Source - Env.worldMap.stream ? Stream.GetPublisher mapTo manifest[Stream.PublisherType] map { publisher => - val source = Source fromPublisher publisher + Env.worldMap.getSource map { source => Ok.chunked(source via EventSource.flow).as("text/event-stream") } } diff --git a/app/views/site/worldMap.scala.html b/app/views/site/worldMap.scala.html index 6c6df523f8..77ccb077f6 100644 --- a/app/views/site/worldMap.scala.html +++ b/app/views/site/worldMap.scala.html @@ -1,7 +1,6 @@ @() - Lichess World Map diff --git a/modules/common/src/main/AkkaStream.scala b/modules/common/src/main/AkkaStream.scala index 819dcea541..1dd89bdcd0 100644 --- a/modules/common/src/main/AkkaStream.scala +++ b/modules/common/src/main/AkkaStream.scala @@ -9,7 +9,7 @@ object AkkaStream { def actorPublisher[T]( bufferSize: Int, - overflowStrategy: OverflowStrategy)(implicit materializer: Materializer): (ActorRef, Publisher[T]) = { + overflowStrategy: OverflowStrategy = OverflowStrategy.dropHead)(implicit materializer: Materializer): (ActorRef, Publisher[T]) = { Source.actorRef[T](20, OverflowStrategy.dropHead) .toMat(Sink asPublisher false)(Keep.both).run() } diff --git a/modules/common/src/main/PackageObject.scala b/modules/common/src/main/PackageObject.scala index e7a3e993ee..75089730ea 100644 --- a/modules/common/src/main/PackageObject.scala +++ b/modules/common/src/main/PackageObject.scala @@ -174,6 +174,8 @@ trait WithPlay { self: PackageObject => implicit final class LilaPimpedActorSystem(self: akka.actor.ActorSystem) { def lilaBus = lila.common.Bus(self) + + def lilaMat = materializer(self) } object makeTimeout { diff --git a/modules/worldMap/src/main/Env.scala b/modules/worldMap/src/main/Env.scala index b0b35e22f3..8a6cb75026 100644 --- a/modules/worldMap/src/main/Env.scala +++ b/modules/worldMap/src/main/Env.scala @@ -17,7 +17,16 @@ final class Env( Props(new Stream( geoIp = MaxMindIpGeo(GeoIPFile, 0), geoIpCacheTtl = GeoIPCacheTtl))) - system.lilaBus.subscribe(stream, 'changeFeaturedGame, 'streams, 'nbMembers, 'nbRounds) + + system.lilaBus.subscribe(stream, 'roundDoor) + + import akka.pattern.ask + import akka.stream.scaladsl.Source + import scala.concurrent.duration._ + import play.api.libs.json.JsValue + implicit val timeout = akka.util.Timeout(5 seconds) + def getSource: Fu[Stream.SourceType] = + stream ? Stream.GetSource mapTo manifest[Stream.SourceType] } object Env { diff --git a/modules/worldMap/src/main/Stream.scala b/modules/worldMap/src/main/Stream.scala index 8970106704..ff52c15d7d 100644 --- a/modules/worldMap/src/main/Stream.scala +++ b/modules/worldMap/src/main/Stream.scala @@ -15,60 +15,58 @@ private final class Stream( geoIp: MaxMindIpGeo, geoIpCacheTtl: Duration) extends Actor { - import Stream.game2json + import Stream._ + implicit val mat = context.system.lilaMat - val games = scala.collection.mutable.Map.empty[String, Stream.Game] + val games = scala.collection.mutable.Map.empty[String, Game] - val (out, realtime) = - lila.common.AkkaStream.actorSource(20, OverflowStrategy.dropHead)(materializer(context.system)) + val (out, publisher) = lila.common.AkkaStream.actorPublisher[Event](100) - def preload = Source[JsValue](games.values.map(game2json(makeMd5)).toList) - val preloadComplete = Source[JsValue](List(Json.obj("loadComplete" -> true): JsValue)) + def preload = Source[JsValue](games.values.pp.map(game2json(makeMd5)).toList.pp) + val preloadComplete = Source[JsValue](List(Json.obj("loadComplete" -> true))) - val transformer = Flow.fromFunction[Stream.Event, JsValue] { - case Stream.Event.Add(game) => game2json(makeMd5)(game) - case Stream.Event.Remove(id) => Json.obj("id" -> id) + val transformer = Flow.fromFunction[Event, JsValue] { + case Event.Add(game) => game2json(makeMd5)(game) + case Event.Remove(id) => Json.obj("id" -> id) } + def realtime = Source.fromPublisher[Event](publisher) + def makeSource = Source.combine(preload, preloadComplete, realtime via transformer)(_ => Concat()) - def makePublisher = - makeSource.toMat(Sink asPublisher false)(Keep.both).run() - def receive = { case SocketEvent.OwnerJoin(id, color, ip) => - ipCache get ip foreach { point => + (ipCache get "58.8.28.97").pp foreach { point => val game = games get id match { case Some(game) => game withPoint point - case None => Stream.Game(id, List(point)) + case None => Game(id, List(point)) } games += (id -> game) - out ! Stream.Event.Add(game) + out ! Event.Add(game).pp } case SocketEvent.Stop(id) => games -= id - out ! Stream.Event.Remove(id) + out ! Event.Remove(id) - case Stream.GetPublisher => sender ! makePublisher + case GetSource => + println(games) + sender ! makeSource } - implicit val mat = materializer(context.system) - def makeMd5 = MessageDigest getInstance "MD5" val ipCache = lila.memo.Builder.cache(geoIpCacheTtl, ipToPoint) - def ipToPoint(ip: String): Option[Stream.Point] = - geoIp getLocation ip flatMap Stream.toPoint + def ipToPoint(ip: String): Option[Point] = + geoIp getLocation ip flatMap toPoint } object Stream { - case object GetPublisher + case object GetSource - import org.reactivestreams.Publisher - type PublisherType = Publisher[JsValue] + type SourceType = Source[JsValue, akka.NotUsed] case class Game(id: String, points: List[Point]) { diff --git a/public/worldMap/app.js b/public/worldMap/app.js index e4a23e8d04..cdffbcc453 100644 --- a/public/worldMap/app.js +++ b/public/worldMap/app.js @@ -39,7 +39,7 @@ $(function() { point[1] + Math.random() - 0.5); }; - var source = new EventSource("http://en.lichess.org/network/stream"); + var source = new EventSource("http://" + location.hostname + "/network/stream"); var removeFunctions = {};