fix user TV race condition - closes #4614

If the game finishes between page load and websocket connection,
the spectator can remain stuck on the finished game.

Now checking if a new game is available on websocket connection.
This commit is contained in:
Thibault Duplessis 2018-11-11 13:01:33 +01:00
parent bd1817392a
commit 0f9c6003e4
8 changed files with 42 additions and 19 deletions

View file

@ -237,7 +237,7 @@ private[controllers] trait LilaController
}
protected def NoCurrentGame(a: => Fu[Result])(implicit ctx: Context): Fu[Result] =
ctx.me.??(mashup.Preload.currentGame(Env.user.lightUserSync)) flatMap {
ctx.me.??(mashup.Preload.currentGameMyTurn(Env.user.lightUserSync)) flatMap {
_.fold(a) { current =>
negotiate(
html = Lobby.renderHome(Results.Forbidden),

View file

@ -21,12 +21,18 @@ object Round extends LilaController with TheftPrevention {
proxyPov(gameId, color) flatMap {
_ ?? { pov =>
getSocketUid("sri") ?? { uid =>
val userTv = get("userTv") map { userId =>
lila.round.actorApi.UserTv(
userId,
pov.game.finishedOrAborted ?? GameRepo.lastPlayedPlaying(userId).map(_.isDefined)
)
}
env.socketHandler.watcher(
pov = pov,
uid = uid,
user = ctx.me,
ip = ctx.ip,
userTv = get("userTv"),
userTv = userTv,
version = getSocketVersion
) map some
}

View file

@ -49,7 +49,7 @@ final class Preload(
liveStreams().dmap(_.autoFeatured.withTitles(lightUserApi)) zip
(ctx.userId ?? getPlayban) flatMap {
case (data, povs) ~ posts ~ tours ~ events ~ simuls ~ feat ~ entries ~ lead ~ tWinners ~ puzzle ~ streams ~ playban =>
val currentGame = ctx.me ?? Preload.currentGame(povs, lightUserApi.sync) _
val currentGame = ctx.me ?? Preload.currentGameMyTurn(povs, lightUserApi.sync) _
lightUserApi.preloadMany {
tWinners.map(_.userId) :::
posts.flatMap(_.userId) :::
@ -63,12 +63,12 @@ object Preload {
case class CurrentGame(pov: Pov, json: JsObject, opponent: String)
def currentGame(lightUser: lila.common.LightUser.GetterSync)(user: User): Fu[Option[CurrentGame]] =
def currentGameMyTurn(lightUser: lila.common.LightUser.GetterSync)(user: User): Fu[Option[CurrentGame]] =
GameRepo.playingRealtimeNoAi(user, 10) map {
currentGame(_, lightUser)(user)
currentGameMyTurn(_, lightUser)(user)
}
def currentGame(povs: List[Pov], lightUser: lila.common.LightUser.GetterSync)(user: User): Option[CurrentGame] =
private def currentGameMyTurn(povs: List[Pov], lightUser: lila.common.LightUser.GetterSync)(user: User): Option[CurrentGame] =
povs.collectFirst {
case pov if pov.game.nonAi && pov.game.hasClock && pov.isMyTurn =>
val opponent = lila.game.Namer.playerText(pov.opponent)(lightUser)

View file

@ -1,8 +1,8 @@
package lila.common
import scala.concurrent.duration._
import akka.actor.ActorSystem
import play.api.libs.iteratee._
import scala.concurrent.duration._
object Iteratee {
@ -15,4 +15,10 @@ object Iteratee {
def prepend[A](elements: Seq[A], enumerator: Enumerator[A]): Enumerator[A] =
if (elements.isEmpty) enumerator
else Enumerator(elements: _*) >>> enumerator
// Avoid running `andThen` on empty elements, just for perf
def prependFu[A](elements: Fu[Seq[A]], enumerator: Enumerator[A]): Enumerator[A] =
Enumerator flatten {
elements map { prepend(_, enumerator) }
}
}

View file

@ -170,11 +170,13 @@ object GameRepo {
// gets last recently played move game in progress
def lastPlayedPlaying(user: User): Fu[Option[Pov]] =
coll.find(Query recentlyPlaying user.id)
lastPlayedPlaying(user.id).map { _ flatMap { Pov(_, user) } }
def lastPlayedPlaying(userId: User.ID): Fu[Option[Game]] =
coll.find(Query recentlyPlaying userId)
.sort(Query.sortMovedAtNoIndex)
.cursor[Game](readPreference = ReadPreference.secondaryPreferred)
.uno
.map { _ flatMap { Pov(_, user) } }
def allPlaying(userId: User.ID): Fu[List[Pov]] =
coll.find(Query nowPlaying userId).list[Game]()

View file

@ -185,11 +185,18 @@ private[round] final class Socket(
case Join(uid, user, color, playerId, ip, onTv, version) =>
val (enumerator, channel) = Concurrent.broadcast[JsValue]
val member = Member(channel, user, color, playerId, ip, onTv)
val member = Member(channel, user, color, playerId, ip, onTv.map(_.userId))
addMember(uid, member)
notifyCrowd
if (playerId.isDefined) playerDo(color, _.ping)
if (member.userTv.isDefined) buscriptions.tv
val reloadTvEvent = onTv ?? {
case UserTv(_, reload) => reload map {
case true => makeMessage("resync").some
case false =>
buscriptions.tv // reload buscriptions
none
}
}
val events = version.fold(history.getRecentEvents(5).some) {
history.getEventsSince
}
@ -198,11 +205,13 @@ private[round] final class Socket(
batchMsgs(member, _)
} map { m => Enumerator(m: JsValue) }
sender ! Connected(
initialMsgs.fold(enumerator) { _ >>> enumerator },
member
val fullEnumerator = lila.common.Iteratee.prependFu(
reloadTvEvent.map(_.toList),
initialMsgs.fold(enumerator) { _ >>> enumerator }
)
sender ! Connected(fullEnumerator, member)
case Nil =>
case eventList: EventList => notify(eventList.events)

View file

@ -125,7 +125,7 @@ private[round] final class SocketHandler(
uid: Uid,
user: Option[User],
ip: IpAddress,
userTv: Option[User.ID],
userTv: Option[UserTv],
version: Option[SocketVersion]
): Fu[JsSocketHandler] = join(pov, none, uid, user, ip, userTv, version)
@ -144,7 +144,7 @@ private[round] final class SocketHandler(
uid: Uid,
user: Option[User],
ip: IpAddress,
userTv: Option[User.ID],
userTv: Option[UserTv],
version: Option[SocketVersion]
): Fu[JsSocketHandler] = {
val join = Join(

View file

@ -7,13 +7,12 @@ import chess.format.Uci
import chess.{ MoveMetrics, Color }
import lila.common.IpAddress
import lila.game.Event
import lila.socket.Socket.Uid
import lila.socket.SocketMember
import lila.user.User
import lila.socket.Socket.SocketVersion
case class EventList(events: List[Event])
case class EventList(events: List[lila.game.Event])
sealed trait Member extends SocketMember {
@ -77,9 +76,10 @@ case class Join(
color: Color,
playerId: Option[String],
ip: IpAddress,
userTv: Option[User.ID],
userTv: Option[UserTv],
version: Option[SocketVersion]
)
case class UserTv(userId: User.ID, reload: Fu[Boolean])
case class Connected(enumerator: JsEnumerator, member: Member)
case class Bye(color: Color)
case class IsGone(color: Color)