Skip to content

Commit

Permalink
Add a mutex to ServerGame to avoid potential concurrency issues
Browse files Browse the repository at this point in the history
  • Loading branch information
Zomis committed Jul 13, 2020
1 parent a0ac0db commit 91e20fb
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 58 deletions.
Expand Up @@ -2,6 +2,8 @@ package net.zomis.games.server2.games

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import klog.KLoggers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.withLock
import net.zomis.core.events.EventSystem
import net.zomis.games.dsl.Actionable
import net.zomis.games.dsl.GameSpec
Expand All @@ -15,6 +17,70 @@ class DslGameSystem<T : Any>(val name: String, val dsl: GameSpec<T>) {

private val mapper = jacksonObjectMapper()
private val logger = KLoggers.logger(this)

fun perform(events: EventSystem, it: PlayerGameMoveRequest) {
val serverGame = it.game
val controller = it.game.obj as GameImpl<T>
if (controller.isGameOver()) {
events.execute(it.illegalMove("Game already finished"))
return
}

val actionType = controller.actions.type(it.moveType)
if (actionType == null) {
events.execute(it.illegalMove("No such actionType: ${it.moveType}"))
return
}

val action: Actionable<T, Any>
try {
action = if (actionType.parameterClass == Unit::class) {
actionType.createAction(it.player, Unit)
} else {
if (actionType.actionType.parameterType.isInstance(it.move)) {
actionType.createAction(it.player, actionType.actionType.parameterType.cast(it.move))
} else if (it.serialized) {
// it.move is a JsonNode
val serializedMove = mapper.convertValue(it.move, actionType.actionType.serializedType.java)
actionType.createActionFromSerialized(it.player, serializedMove)
} else {
throw UnsupportedOperationException("Unknown object of type " + it.move.javaClass + " serialized? " + it.serialized)
}
}
} catch (e: Exception) {
logger.error(e, "Error reading move: $it")
return
}

if (!actionType.isAllowed(action)) {
events.execute(it.illegalMove("Action is not allowed"))
return
}

val beforeMoveEliminated = controller.eliminationCallback.eliminations()
try {
controller.stateKeeper.clear() // TODO: Remove this and use replayable
events.execute(PreMoveEvent(it.game, it.player, it.moveType, action.parameter))
actionType.perform(action)
if (controller.stateKeeper.logs().size > 20) { throw IllegalStateException("${controller.stateKeeper.logs().size}") }
controller.stateKeeper.logs().forEach { log -> sendLogs(serverGame, log) }
} catch (e: Exception) {
logger.error(e) { "Error processing move $it" }
events.execute(it.illegalMove("Error occurred while processing move: $e"))
}
val recentEliminations = controller.eliminationCallback.eliminations().minus(beforeMoveEliminated)

events.execute(MoveEvent(it.game, it.player, it.moveType, action.parameter))
for (elimination in recentEliminations) {
events.execute(PlayerEliminatedEvent(it.game, elimination.playerIndex,
elimination.winResult, elimination.position))
}

if (controller.isGameOver()) {
events.execute(GameEndedEvent(it.game))
}
}

fun setup(events: EventSystem) {
val server2GameName = name
val setup = GameSetupImpl(dsl)
Expand All @@ -25,65 +91,10 @@ class DslGameSystem<T : Any>(val name: String, val dsl: GameSpec<T>) {
events.listen("DslGameSystem $name Move", PlayerGameMoveRequest::class, {
it.game.gameType.type == server2GameName
}, {
val serverGame = it.game
val controller = it.game.obj as GameImpl<T>
if (controller.isGameOver()) {
events.execute(it.illegalMove("Game already finished"))
return@listen
}

val actionType = controller.actions.type(it.moveType)
if (actionType == null) {
events.execute(it.illegalMove("No such actionType: ${it.moveType}"))
return@listen
}

val action: Actionable<T, Any>
try {
action = if (actionType.parameterClass == Unit::class) {
actionType.createAction(it.player, Unit)
} else {
if (actionType.actionType.parameterType.isInstance(it.move)) {
actionType.createAction(it.player, actionType.actionType.parameterType.cast(it.move))
} else if (it.serialized) {
// it.move is a JsonNode
val serializedMove = mapper.convertValue(it.move, actionType.actionType.serializedType.java)
actionType.createActionFromSerialized(it.player, serializedMove)
} else {
throw UnsupportedOperationException("Unknown object of type " + it.move.javaClass + " serialized? " + it.serialized)
}
runBlocking {
it.game.mutex.withLock {
perform(events, it)
}
} catch (e: Exception) {
logger.error(e, "Error reading move: $it")
return@listen
}

if (!actionType.isAllowed(action)) {
events.execute(it.illegalMove("Action is not allowed"))
return@listen
}

val beforeMoveEliminated = controller.eliminationCallback.eliminations()
try {
controller.stateKeeper.clear() // TODO: Remove this and use replayable
events.execute(PreMoveEvent(it.game, it.player, it.moveType, action.parameter))
actionType.perform(action)
if (controller.stateKeeper.logs().size > 20) { throw IllegalStateException("${controller.stateKeeper.logs().size}") }
controller.stateKeeper.logs().forEach { log -> sendLogs(serverGame, log) }
} catch (e: Exception) {
logger.error(e) { "Error processing move $it" }
events.execute(it.illegalMove("Error occurred while processing move: $e"))
}
val recentEliminations = controller.eliminationCallback.eliminations().minus(beforeMoveEliminated)

events.execute(MoveEvent(it.game, it.player, it.moveType, action.parameter))
for (elimination in recentEliminations) {
events.execute(PlayerEliminatedEvent(it.game, elimination.playerIndex,
elimination.winResult, elimination.position))
}

if (controller.isGameOver()) {
events.execute(GameEndedEvent(it.game))
}
})
events.listen("DslGameSystem register $name", StartupEvent::class, {true}, {
Expand Down
@@ -1,6 +1,7 @@
package net.zomis.games.server2.games

import klog.KLoggers
import kotlinx.coroutines.sync.Mutex
import net.zomis.core.events.EventSystem
import net.zomis.core.events.ListenerPriority
import net.zomis.games.Features
Expand Down Expand Up @@ -44,6 +45,7 @@ class ServerGame(private val callback: GameCallback, val gameType: GameType, val
.handler("viewRequest", this::viewRequest)
var gameOver: Boolean = false
private val nextMoveIndex = AtomicInteger(0)
val mutex = Mutex()
internal val players: MutableList<Client> = mutableListOf()
internal val observers: MutableSet<Client> = mutableSetOf()
// TODO: Declare as GameReplayable? Only DSL-games are used anyway.
Expand Down

0 comments on commit 91e20fb

Please sign in to comment.