Compare commits

...

2 Commits
4.7.2 ... 4.7.3

Author SHA1 Message Date
TeamCity
49a1bd40ff ci: bump version to v4.7.3 2025-12-04 01:32:05 +00:00
f847424b9c fix: BAC-25 Race Condition: Websocket Promises (#99)
Reviewed-on: #99
Reviewed-by: lq64 <lq@blackhole.local>
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-04 02:29:19 +01:00
4 changed files with 72 additions and 44 deletions

View File

@@ -199,3 +199,8 @@
* **ui:** FRO-7 Endscreen ([#97](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/97)) ([d57e6ef](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d57e6efa985ca07c32f9f54595fe7393dbdf4d8a)) * **ui:** FRO-7 Endscreen ([#97](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/97)) ([d57e6ef](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d57e6efa985ca07c32f9f54595fe7393dbdf4d8a))
## (2025-12-03) ## (2025-12-03)
## (2025-12-03) ## (2025-12-03)
## (2025-12-04)
### Bug Fixes
* BAC-25 Race Condition: Websocket Promises ([#99](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/99)) ([f847424](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/f847424b9cea423ace5661d1efb6e4f01483c655))

View File

@@ -26,6 +26,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
else canInteract = Some(InteractionType.Card) else canInteract = Some(InteractionType.Card)
case _ => case _ =>
} }
websocketActor.foreach(_.solveRequests())
websocketActor.foreach(_.transmitEventToClient(event)) websocketActor.foreach(_.transmitEventToClient(event))
} }
@@ -38,49 +39,41 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
} }
def handleWebResponse(eventType: String, data: JsObject): Unit = { def handleWebResponse(eventType: String, data: JsObject): Unit = {
lock.lock() eventType match {
val result = Try { case "ping" =>
eventType match { // No action needed for Ping
case "ping" => ()
// No action needed for Ping case "StartGame" =>
() gameLobby.startGame(user)
case "StartGame" => case "PlayCard" =>
gameLobby.startGame(user) val maybeCardIndex: Option[String] = (data \ "cardindex").asOpt[String]
case "PlayCard" => maybeCardIndex match {
val maybeCardIndex: Option[String] = (data \ "cardindex").asOpt[String] case Some(index) =>
maybeCardIndex match { val session = gameLobby.getUserSession(user.id)
case Some(index) => gameLobby.playCard(session, index.toInt)
val session = gameLobby.getUserSession(user.id) case None =>
gameLobby.playCard(session, index.toInt) println("Card Index not found or is not a number.")
case None => }
println("Card Index not found or is not a number.") case "PickTrumpsuit" =>
} val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int]
case "PickTrumpsuit" => maybeSuitIndex match {
val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int] case Some(index) =>
maybeSuitIndex match { val session = gameLobby.getUserSession(user.id)
case Some(index) => gameLobby.selectTrump(session, index)
val session = gameLobby.getUserSession(user.id) case None =>
gameLobby.selectTrump(session, index) println("Card Index not found or is not a number.")
case None => }
println("Card Index not found or is not a number.") case "KickPlayer" =>
} val maybePlayerId: Option[String] = (data \ "playerId").asOpt[String]
case "KickPlayer" => maybePlayerId match {
val maybePlayerId: Option[String] = (data \ "playerId").asOpt[String] case Some(id) =>
maybePlayerId match { val playerUUID = UUID.fromString(id)
case Some(id) => gameLobby.leaveGame(playerUUID, true)
val playerUUID = UUID.fromString(id) case None =>
gameLobby.leaveGame(playerUUID, true) println("Player ID not found or is not a valid UUID.")
case None => }
println("Player ID not found or is not a valid UUID.") case "ReturnToLobby" =>
} gameLobby.returnToLobby(this)
case "ReturnToLobby" =>
gameLobby.returnToLobby(this)
}
}
lock.unlock()
if (result.isFailure) {
val throwable = result.failed.get
throw throwable
} }
} }

View File

@@ -5,6 +5,7 @@ import org.apache.pekko.actor.{Actor, ActorRef}
import play.api.libs.json.{JsObject, JsValue, Json} import play.api.libs.json.{JsObject, JsValue, Json}
import util.WebsocketEventMapper import util.WebsocketEventMapper
import scala.collection.mutable
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
class UserWebsocketActor( class UserWebsocketActor(
@@ -12,6 +13,8 @@ class UserWebsocketActor(
session: UserSession session: UserSession
) extends Actor { ) extends Actor {
private val requests: mutable.Map[String, String] = mutable.Map()
{ {
session.lock.lock() session.lock.lock()
if (session.websocketActor.isDefined) { if (session.websocketActor.isDefined) {
@@ -48,12 +51,14 @@ class UserWebsocketActor(
} }
private def handle(json: JsValue): Unit = { private def handle(json: JsValue): Unit = {
session.lock.lock()
val idOpt = (json \ "id").asOpt[String] val idOpt = (json \ "id").asOpt[String]
if (idOpt.isEmpty) { if (idOpt.isEmpty) {
transmitJsonToClient(Json.obj( transmitJsonToClient(Json.obj(
"status" -> "error", "status" -> "error",
"error" -> "Missing 'id' field" "error" -> "Missing 'id' field"
)) ))
session.lock.unlock()
return return
} }
val id = idOpt.get val id = idOpt.get
@@ -65,17 +70,25 @@ class UserWebsocketActor(
"status" -> "error", "status" -> "error",
"error" -> "Missing 'event' field" "error" -> "Missing 'event' field"
)) ))
session.lock.unlock()
return return
} }
val statusOpt = (json \ "status").asOpt[String] val statusOpt = (json \ "status").asOpt[String]
if (statusOpt.isDefined) { if (statusOpt.isDefined) {
session.lock.unlock()
return return
} }
val event = eventOpt.get val event = eventOpt.get
val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj()) val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj())
requests += (id -> event)
val result = Try { val result = Try {
session.handleWebResponse(event, data) session.handleWebResponse(event, data)
} }
if (!requests.contains(id)) {
session.lock.unlock()
return
}
requests -= id
if (result.isSuccess) { if (result.isSuccess) {
transmitJsonToClient(Json.obj( transmitJsonToClient(Json.obj(
"id" -> id, "id" -> id,
@@ -90,6 +103,7 @@ class UserWebsocketActor(
"error" -> result.failed.get.getMessage "error" -> result.failed.get.getMessage
)) ))
} }
session.lock.unlock()
} }
def transmitJsonToClient(jsonObj: JsValue): Unit = { def transmitJsonToClient(jsonObj: JsValue): Unit = {
@@ -100,4 +114,20 @@ class UserWebsocketActor(
transmitJsonToClient(WebsocketEventMapper.toJson(event, session)) transmitJsonToClient(WebsocketEventMapper.toJson(event, session))
} }
def solveRequests(): Unit = {
if (!session.lock.isHeldByCurrentThread)
return;
if (requests.isEmpty)
return;
val pendingRequests = requests.toMap
requests.clear()
pendingRequests.foreach { case (id, event) =>
transmitJsonToClient(Json.obj(
"id" -> id,
"event" -> event,
"status" -> "success"
))
}
}
} }

View File

@@ -1,3 +1,3 @@
MAJOR=4 MAJOR=4
MINOR=7 MINOR=7
PATCH=2 PATCH=3