Skip to content

Commit

Permalink
Cache bingo boards
Browse files Browse the repository at this point in the history
  • Loading branch information
timoschwarzer committed Aug 23, 2024
1 parent 4cd1b14 commit 6a8ed14
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/wotw/server/api/BingoEndpoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class BingoEndpoint(server: WotwBackendServer) : Endpoint(server) {
val player = authenticatedUserOrNull()

val multiverse = Multiverse.findById(multiverseId) ?: throw NotFoundException()
multiverse.board ?: throw NotFoundException()
multiverse.cachedBoard ?: throw NotFoundException()
val info = multiverse.bingoUniverseInfo()

val currentPlayerUniverseInThisMultiverse = player?.id?.value?.let { playerId ->
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/wotw/server/api/BingothonEndpoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class BingothonEndpoint(server: WotwBackendServer) : Endpoint(server) {
val token = BingothonToken.findById(tokenId) ?: throw NotFoundException()
val player = token.owner
val multiverse = token.multiverse
val board = multiverse.board ?: throw BadRequestException("No bingo board attached to current multiverse")
val board = multiverse.cachedBoard ?: throw BadRequestException("No bingo board attached to current multiverse")
val playerIsSpectator = multiverse.spectators.contains(player)

if (board.size != 5) {
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/wotw/server/api/DeveloperEndpoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import wotw.io.messages.ClaimBingoCardRequest
import wotw.io.messages.CreateLeagueSeasonRequest
import wotw.io.messages.admin.RemoteTrackerEndpointDescriptor
import wotw.server.database.model.BingoCardClaim
import wotw.server.database.model.LeagueGameSubmission
import wotw.server.database.model.LeagueSeason
import wotw.server.database.model.User
import wotw.server.main.WotwBackendServer
Expand Down Expand Up @@ -42,7 +41,7 @@ class DeveloperEndpoint(server: WotwBackendServer) : Endpoint(server) {
val user = authenticatedUser()
val multiverse = user.mostRecentMultiverse ?: throw BadRequestException("You are currently not in a multiverse")
val universe = user.mostRecentWorldMembership?.world?.universe ?: throw BadRequestException("You are currently not in a multiverse")
val board = multiverse.board ?: throw NotFoundException("The multiverse you are in does not have a bingo board")
val board = multiverse.cachedBoard ?: throw NotFoundException("The multiverse you are in does not have a bingo board")
val claims = multiverse.bingoCardClaims
val multiverseId = multiverse.id.value

Expand Down
25 changes: 15 additions & 10 deletions src/main/kotlin/wotw/server/database/model/Multiverse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ import wotw.server.bingo.plus
import wotw.server.game.handlers.GameHandlerType
import wotw.server.sync.UniverseStateCache
import wotw.server.util.assertTransaction
import wotw.util.ExpiringCache
import java.util.*
import kotlin.math.ceil
import kotlin.time.Duration.Companion.minutes

val bingoBoardCache = ExpiringCache<Long, BingoBoard?>(20.minutes)

object Multiverses : LongIdTable("multiverses") {
val seedId = reference("seed_id", Seeds).nullable()
Expand Down Expand Up @@ -67,10 +71,13 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
val playersAndSpectators
get() = players + spectators

val cachedBoard
get() = bingoBoardCache.getOrPut(id.value) { board }

suspend fun getNewBingoCardClaims(universe: Universe): List<BingoCardClaim> {
val newClaims = mutableListOf<BingoCardClaim>()

val board = board ?: return emptyList()
val board = cachedBoard ?: return emptyList()
val state =
UniverseStateCache.get(universe.id.value)//universeStates[universe]?.uberStateData ?: UberStateMap.empty
val claimedSquarePositions = bingoCardClaims.filter { it.universe == universe }.map { it.x to it.y }
Expand Down Expand Up @@ -99,7 +106,7 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
targetUniverse: Universe?,
spectator: Boolean = false
): BingoBoardMessage {
val board = board ?: return BingoBoardMessage()
val board = cachedBoard ?: return BingoBoardMessage()

val targetUniverseState = targetUniverse?.let { u -> UniverseStateCache.get(u.id.value) } ?: UberStateMap.empty

Expand Down Expand Up @@ -192,9 +199,7 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
// all of them. Goals with revealed neighbors are handled above.
if (board.config.lockout) {
ownCardClaims.forEach { claim ->
goals[claim.x to claim.y]?.let { goal ->
goal.visibleFor.add(universe.id.value)
}
goals[claim.x to claim.y]?.visibleFor?.add(universe.id.value)
}
}
}
Expand All @@ -214,7 +219,7 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
}

private fun goalCompletionMap(): Map<Pair<Int, Int>, Set<Universe>> {
val board = board ?: return emptyMap()
val board = cachedBoard ?: return emptyMap()
val events = bingoCardClaims
return (1..board.size).flatMap { x ->
(1..board.size).map { y ->
Expand All @@ -226,7 +231,7 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
}

fun getLockoutGoalOwnerMap(): Map<Pair<Int, Int>, Universe?> {
val board = board ?: return emptyMap()
val board = cachedBoard ?: return emptyMap()
val owners = (1..board.size).flatMap { x ->
(1..board.size).map { y ->
x to y
Expand All @@ -238,13 +243,13 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
}

fun scoreRelevantCompletionMap() =
(if (board?.config?.lockout == true) getLockoutGoalOwnerMap().mapValues {
(if (cachedBoard?.config?.lockout == true) getLockoutGoalOwnerMap().mapValues {
val universe = it.value ?: return@mapValues emptySet()
setOf(universe)
} else goalCompletionMap())

fun bingoUniverseInfo(universe: Universe): BingoUniverseInfo {
val board = board ?: return BingoUniverseInfo(universe.id.value, "")
val board = cachedBoard ?: return BingoUniverseInfo(universe.id.value, "")
val lockout = board.config.lockout
val completions = scoreRelevantCompletionMap().filterValues { universe in it }.keys.toSet()

Expand Down Expand Up @@ -322,7 +327,7 @@ class Multiverse(id: EntityID<Long>) : LongEntity(id) {
fun updateAutomaticWorldNames() {
assertTransaction()

var nextEmptyWorldNumber = 1;
var nextEmptyWorldNumber = 1
for (world in worlds) {
if (world.hasCustomName) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransacti
import wotw.io.messages.json
import wotw.io.messages.protobuf.*
import wotw.server.api.*
import wotw.server.bingo.Point
import wotw.server.bingo.UberStateMap
import wotw.server.database.model.*
import wotw.server.exception.ConflictException
Expand Down Expand Up @@ -424,7 +423,7 @@ class NormalGameHandler(multiverseId: Long, server: WotwBackendServer) : GameHan

val results = server.sync.aggregateStates(worldMembership, uberStates)

getMultiverse().board?.let { board ->
getMultiverse().cachedBoard?.let { board ->
val newBingoCardClaims = world.universe.multiverse.getNewBingoCardClaims(world.universe)

if (board.config.lockout) {
Expand Down Expand Up @@ -482,7 +481,7 @@ class NormalGameHandler(multiverseId: Long, server: WotwBackendServer) : GameHan

// Add bingo states if we have a bingo game
newSuspendedTransaction {
Multiverse.findById(multiverseId)?.board?.goals?.flatMap { it.value.keys }
Multiverse.findById(multiverseId)?.cachedBoard?.goals?.flatMap { it.value.keys }
}?.let {
aggregationRegistry += AggregationStrategyRegistry().apply {
register(
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/wotw/server/main/WotwBackendServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class WotwBackendServer {

val cacheScheduler = Scheduler {
sync.purgeCache(60)
bingoBoardCache.garbageCollect()

gameHandlerRegistry.cacheEntries.filter { cacheEntry ->
cacheEntry.isDisposable().also { disposable ->
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/wotw/server/opher/OpherAutobanController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class OpherAutobanController(val server: WotwBackendServer) {
private fun hashMessage(message: Message): MessageHash = message.content.trim().md5()

suspend fun reportMessage(message: Message): MessageBurstInfo {
val burstInfo = messageBurstsInChannels.getOrPut(hashMessage(message)) { MessageBurstInfo() }
val burstInfo = messageBurstsInChannels.getOrPutSuspended(hashMessage(message)) { MessageBurstInfo() }
burstInfo.channels[message.channelId] = Clock.System.now()
burstInfo.messages += message
return burstInfo
Expand Down Expand Up @@ -95,7 +95,7 @@ class OpherAutobanController(val server: WotwBackendServer) {

val author = message.author ?: return@on

val guildId = channelIdGuildIdCache.getOrTryPut(message.channelId) {
val guildId = channelIdGuildIdCache.getOrTryPutSuspended(message.channelId) {
message.getGuildOrNull()?.id
}

Expand All @@ -105,7 +105,7 @@ class OpherAutobanController(val server: WotwBackendServer) {

val memberId = MemberId(guildId, author.id)

val recentCommunication = rateLimitCache.getOrPut(memberId) { RecentMemberCommunication() }
val recentCommunication = rateLimitCache.getOrPutSuspended(memberId) { RecentMemberCommunication() }
recentCommunication.garbageCollect()

val burstInfo = recentCommunication.reportMessage(message)
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/wotw/server/services/InfoMessagesService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class InfoMessagesService(private val server: WotwBackendServer) {

fun generateMultiverseMetadataInfoMessage(multiverse: Multiverse) = MultiverseMetadataInfoMessage(
multiverse.id.value,
multiverse.board != null,
multiverse.cachedBoard != null,
multiverse.seed != null,
multiverse.memberships.map { generateUserInfo(it.user) },
multiverse.createdAt.toEpochMilli(),
Expand All @@ -47,7 +47,7 @@ class InfoMessagesService(private val server: WotwBackendServer) {
multiverse.id.value,
multiverse.universes.sortedBy { it.id }
.mapIndexed { index, universe -> generateUniverseInfo(universe, COLORS[index % COLORS.size]) },
multiverse.board != null,
multiverse.cachedBoard != null,
multiverse.spectators.map(::generateUserInfo),
multiverse.seed?.takeIf { it.allowDownload }?.id?.value,
multiverse.gameHandlerType,
Expand Down
38 changes: 26 additions & 12 deletions src/main/kotlin/wotw/server/sync/StateSynchronization.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package wotw.server.sync

import kotlinx.html.currentTimeMillis
import org.jetbrains.exposed.dao.load
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import wotw.io.messages.protobuf.*
import wotw.server.api.AggregationStrategyRegistry
Expand All @@ -9,6 +10,8 @@ import wotw.server.bingo.UberStateMap
import wotw.server.database.EntityCache
import wotw.server.database.model.GameState
import wotw.server.database.model.Multiverse
import wotw.server.database.model.Universe
import wotw.server.database.model.World
import wotw.server.database.model.WorldMembership
import wotw.server.main.WotwBackendServer
import wotw.server.util.assertTransaction
Expand Down Expand Up @@ -68,9 +71,14 @@ class StateSynchronization(private val server: WotwBackendServer) {
worldMembership: WorldMembership,
states: Map<UberId, Double>
): Map<UberId, AggregationResult> {
worldMembership.load(
WorldMembership::world,
World::universe,
Universe::multiverse,
)

val world = worldMembership.world
val universe = world.universe
val multiverse = universe.multiverse
val multiverse = worldMembership.multiverse

var strategies = aggregationStrategiesCache[world.id.value]
if (strategies == null) {
Expand All @@ -81,8 +89,14 @@ class StateSynchronization(private val server: WotwBackendServer) {
return states.flatMap { (uberId, value) ->
strategies.getStrategies(uberId).map { strategy ->
val cache = when (strategy.scope) {
ShareScope.WORLD -> WorldStateCache.getOrNull(world.id.value)
ShareScope.UNIVERSE -> UniverseStateCache.getOrNull(universe.id.value)
ShareScope.WORLD -> {
WorldStateCache.getOrNull(world.id.value)
}

ShareScope.UNIVERSE -> {
UniverseStateCache.getOrNull(world.universe.id.value)
}

ShareScope.MULTIVERSE -> MultiverseStateCache.getOrNull(multiverse.id.value)
ShareScope.PLAYER -> PlayerStateCache.getOrNull(worldMembership.user.id.value)
} ?: return@map uberId to AggregationResult(value, strategy)
Expand All @@ -107,15 +121,15 @@ class StateSynchronization(private val server: WotwBackendServer) {
val playerUpdates = triggered.filterValues {
val strategy = it.strategy
strategy?.group == UberStateSyncStrategy.NotificationGroup.ALL
|| it.sentValue != it.newValue && strategy?.group == UberStateSyncStrategy.NotificationGroup.DIFFERENT
|| it.sentValue != it.newValue && strategy?.group == UberStateSyncStrategy.NotificationGroup.DIFFERENT
}

val shareScopeUpdates = triggered.filterValues {
val strategy = it.strategy
strategy?.group == UberStateSyncStrategy.NotificationGroup.ALL ||
it.oldValue != it.newValue &&
(strategy?.group == UberStateSyncStrategy.NotificationGroup.OTHERS ||
strategy?.group == UberStateSyncStrategy.NotificationGroup.DIFFERENT)
it.oldValue != it.newValue &&
(strategy?.group == UberStateSyncStrategy.NotificationGroup.OTHERS ||
strategy?.group == UberStateSyncStrategy.NotificationGroup.DIFFERENT)
}.entries.groupBy { it.value.strategy?.scope }

shareScopeUpdates.entries.forEach { (scope, states) ->
Expand Down Expand Up @@ -146,7 +160,7 @@ class StateSynchronization(private val server: WotwBackendServer) {
suspend fun syncMultiverseProgress(multiverseId: Long) {
val (syncBingoUniversesMessage, spectatorBoard, stateUpdates) = newSuspendedTransaction {
val multiverse = Multiverse.findById(multiverseId) ?: return@newSuspendedTransaction null
multiverse.board ?: return@newSuspendedTransaction null
multiverse.cachedBoard ?: return@newSuspendedTransaction null

val info = multiverse.bingoUniverseInfo()
val syncBingoUniversesMessage = SyncBingoUniversesMessage(info)
Expand All @@ -166,12 +180,12 @@ class StateSynchronization(private val server: WotwBackendServer) {
UberStateUpdateMessage(
UberId(10, 2),
bingoPlayerData.rank.toDouble()
)
),
),
SyncBoardMessage(
multiverse.createBingoBoardMessage(world.universe),
true
)
),
)
}

Expand All @@ -181,7 +195,7 @@ class StateSynchronization(private val server: WotwBackendServer) {
multiverse.createBingoBoardMessage(null, true),
true
),
worldUpdates
worldUpdates,
)
} ?: return

Expand Down
22 changes: 19 additions & 3 deletions src/main/kotlin/wotw/util/ExpiringCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration
import kotlin.time.toJavaDuration

class ExpiringCache<K, V>(val ttl: Duration) {
private data class CacheEntry<V> (
Expand All @@ -16,13 +15,30 @@ class ExpiringCache<K, V>(val ttl: Duration) {

val values get() = cache.values.map { it.value }

suspend fun getOrPut(key: K, defaultValue: suspend () -> V): V {
suspend fun getOrPutSuspended(key: K, defaultValue: suspend () -> V): V {
return cache.getOrPut(key) {
CacheEntry(defaultValue())
}.value
}

suspend fun getOrTryPut(key: K, defaultValue: suspend () -> V?): V? {
suspend fun getOrTryPutSuspended(key: K, defaultValue: suspend () -> V?): V? {
return cache.getOrElse(key) {
defaultValue()?.let {
cache[key] = CacheEntry(it)
return it
}

return null
}.value
}

fun getOrPut(key: K, defaultValue: () -> V): V {
return cache.getOrPut(key) {
CacheEntry(defaultValue())
}.value
}

fun getOrTryPut(key: K, defaultValue: () -> V?): V? {
return cache.getOrElse(key) {
defaultValue()?.let {
cache[key] = CacheEntry(it)
Expand Down

0 comments on commit 6a8ed14

Please sign in to comment.