From df2dabfdfd1e35397029ed56d7e5ed9062ec858f Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 29 May 2024 15:10:40 -0700 Subject: [PATCH 1/4] Remove mutex from default sqlite (#261) Co-authored-by: Drex --- docs/parameters.md | 2 +- .../commands/arguments/SearchParamArgument.kt | 4 +- .../commands/parameters/SourceParameter.kt | 10 +- .../ledger/database/DatabaseManager.kt | 273 ++++++++++++------ 4 files changed, 187 insertions(+), 102 deletions(-) diff --git a/docs/parameters.md b/docs/parameters.md index 0e705414..00ec7282 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -83,7 +83,7 @@ An easy way to remember the difference between `before:1d` and `after:1d` is to If you go back in time 1 day, do you want everything that happened `before` then or `after` then. Usually you want `after`. -### Rollback Status +## Rollback Status Key - `rolledback:` Value - `true` or `false` Negative Allowed - `No` diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt index 50162aa0..829e6738 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/arguments/SearchParamArgument.kt @@ -27,6 +27,7 @@ import net.minecraft.util.math.BlockBox import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3i import java.time.Instant +import java.util.* import java.util.concurrent.CompletableFuture object SearchParamArgument { @@ -140,7 +141,8 @@ object SearchParamArgument { } } else { val profile = source.server.userCache?.findByName(sourceInput.property) - val id = profile?.orElse(null)?.id + // If the player doesn't exist use a random UUID to make the query not match + val id = profile?.orElse(null)?.id ?: UUID.randomUUID() if (id != null) { val playerIdEntry = Negatable(id, sourceInput.allowed) diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt index 676b013b..3c7d510c 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/parameters/SourceParameter.kt @@ -5,9 +5,9 @@ import com.mojang.brigadier.StringReader import com.mojang.brigadier.context.CommandContext import com.mojang.brigadier.suggestion.Suggestions import com.mojang.brigadier.suggestion.SuggestionsBuilder -import java.util.concurrent.CompletableFuture import net.minecraft.command.CommandSource import net.minecraft.server.command.ServerCommandSource +import java.util.concurrent.CompletableFuture class SourceParameter : SimpleParameter() { override fun parse(stringReader: StringReader): String { @@ -27,14 +27,12 @@ class SourceParameter : SimpleParameter() { val stringReader = StringReader(builder.input) stringReader.cursor = builder.start - val players = context.source.playerNames + val sources = context.source.playerNames DatabaseManager.getKnownSources().forEach { - players.add("@$it") + sources.add("@$it") } - // TODO suggest non-player sources - return CommandSource.suggestMatching( - players, + sources, builder ) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt index 37245960..1eb647c9 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt @@ -13,24 +13,23 @@ import com.github.quiltservertools.ledger.logWarn import com.github.quiltservertools.ledger.registry.ActionRegistry import com.github.quiltservertools.ledger.utility.Negatable import com.github.quiltservertools.ledger.utility.PlayerResult +import com.google.common.cache.Cache import com.mojang.authlib.GameProfile -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.* -import javax.sql.DataSource import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.Mutex import net.minecraft.util.Identifier import net.minecraft.util.WorldSavePath import net.minecraft.util.math.BlockPos +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.SqlLogger @@ -44,7 +43,6 @@ import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertIgnore -import org.jetbrains.exposed.sql.lowerCase import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.orWhere import org.jetbrains.exposed.sql.selectAll @@ -53,10 +51,20 @@ import org.jetbrains.exposed.sql.statements.expandArgs import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update +import org.sqlite.SQLiteConfig import org.sqlite.SQLiteDataSource +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* +import java.util.function.Function +import javax.sql.DataSource import kotlin.io.path.pathString import kotlin.math.ceil +const val MAX_QUERY_RETRIES = 10 +const val MIN_RETRY_DELAY = 1000L +const val MAX_RETRY_DELAY = 300_000L + object DatabaseManager { // These values are initialised late to allow the database to be created at server start, @@ -66,8 +74,6 @@ object DatabaseManager { get() = database.dialect.name private val cache = DatabaseCacheService - private val dbMutex = Mutex() - private var enforceMutex = false fun setup(dataSource: DataSource?) { val source = dataSource ?: getDefaultDatasource() @@ -76,8 +82,9 @@ object DatabaseManager { private fun getDefaultDatasource(): DataSource { val dbFilepath = Ledger.server.getSavePath(WorldSavePath.ROOT).resolve("ledger.sqlite").pathString - enforceMutex = true - return SQLiteDataSource().apply { + return SQLiteDataSource(SQLiteConfig().apply { + setJournalMode(SQLiteConfig.JournalMode.WAL) + }).apply { url = "jdbc:sqlite:$dbFilepath" } } @@ -169,8 +176,8 @@ object DatabaseManager { type.world = Identifier.tryParse(action[Tables.Worlds.identifier]) type.objectIdentifier = Identifier(action[Tables.ObjectIdentifiers.identifier]) type.oldObjectIdentifier = Identifier( - action[Tables.ObjectIdentifiers.alias("oldObjects")[Tables.ObjectIdentifiers.identifier]] - ) + action[Tables.ObjectIdentifiers.alias("oldObjects")[Tables.ObjectIdentifiers.identifier]] + ) type.objectState = action[Tables.Actions.blockState] type.oldObjectState = action[Tables.Actions.oldBlockState] type.sourceName = action[Tables.Sources.name] @@ -209,62 +216,65 @@ object DatabaseManager { op = op.and { Tables.Actions.rolledBack.eq(params.rolledBack) } } - val sourceNames = ArrayList>() - params.sourceNames?.forEach { - val sourceId = getSourceId(it.property) - if (sourceId != null) { - sourceNames.add(Negatable(sourceId, it.allowed)) - } else { - // Unknown source name - op = Op.FALSE - } - } - - val playerNames = ArrayList>() - params.sourcePlayerIds?.forEach { - val playerId = getPlayerId(it.property) - if (playerId != null) { - playerNames.add(Negatable(playerId, it.allowed)) - } else { - // Unknown player name - op = Op.FALSE - } - } - op = addParameters( op, - sourceNames, + params.sourceNames, + DatabaseManager::getSourceId, Tables.Actions.sourceName ) op = addParameters( op, - params.actions?.map { Negatable(getActionId(it.property), it.allowed) }, + params.actions, + DatabaseManager::getActionId, Tables.Actions.actionIdentifier ) op = addParameters( op, - params.worlds?.map { Negatable(getWorldId(it.property), it.allowed) }, + params.worlds, + DatabaseManager::getWorldId, Tables.Actions.world ) op = addParameters( op, - params.objects?.map { Negatable(getRegistryKeyId(it.property), it.allowed) }, + params.objects, + DatabaseManager::getRegistryKeyId, Tables.Actions.objectId, Tables.Actions.oldObjectId ) op = addParameters( op, - playerNames, + params.sourcePlayerIds, + DatabaseManager::getPlayerId, Tables.Actions.sourcePlayer ) return op } + private fun , C : EntityID?, T> addParameters( + op: Op, + paramSet: Collection>?, + objectToId: Function, + column: Column, + orColumn: Column? = null, + ): Op { + val idParamSet = mutableSetOf>() + paramSet?.forEach { + val paramId = objectToId.apply(it.property) + if (paramId != null) { + idParamSet.add(Negatable(paramId, it.allowed)) + } else { + // Unknown source name + return Op.FALSE + } + } + return addParameters(op, idParamSet, column, orColumn) + } + private fun , C : EntityID?> addParameters( op: Op, paramSet: Collection>?, @@ -277,7 +287,6 @@ object DatabaseManager { ): Op { if (allowed.isEmpty()) return op - var operator = if (orColumn != null) { Op.build { column eq allowed.first() or (orColumn eq allowed.first()) } } else { @@ -354,12 +363,15 @@ object DatabaseManager { } private suspend fun execute(body: suspend Transaction.() -> T): T { - if (enforceMutex) dbMutex.lock() while (Ledger.server.overworld?.savingDisabled != false) { delay(timeMillis = 1000) } return newSuspendedTransaction(db = database) { + repetitionAttempts = MAX_QUERY_RETRIES + minRepetitionDelay = MIN_RETRY_DELAY + maxRepetitionDelay = MAX_RETRY_DELAY + if (Ledger.config[DatabaseSpec.logSQL]) { addLogger(object : SqlLogger { override fun log(context: StatementContext, transaction: Transaction) { @@ -368,7 +380,7 @@ object DatabaseManager { }) } body(this) - }.also { if (enforceMutex) dbMutex.unlock() } + } } suspend fun purgeActions(params: ActionSearchParams) { @@ -402,18 +414,18 @@ object DatabaseManager { private fun Transaction.insertActions(actions: List) { Tables.Actions.batchInsert(actions, shouldReturnGeneratedValues = false) { action -> - this[Tables.Actions.actionIdentifier] = getActionId(action.identifier) + this[Tables.Actions.actionIdentifier] = getOrCreateActionId(action.identifier) this[Tables.Actions.timestamp] = action.timestamp this[Tables.Actions.x] = action.pos.x this[Tables.Actions.y] = action.pos.y this[Tables.Actions.z] = action.pos.z - this[Tables.Actions.objectId] = getRegistryKeyId(action.objectIdentifier) - this[Tables.Actions.oldObjectId] = getRegistryKeyId(action.oldObjectIdentifier) - this[Tables.Actions.world] = getWorldId(action.world ?: Ledger.server.overworld.registryKey.value) + this[Tables.Actions.objectId] = getOrCreateRegistryKeyId(action.objectIdentifier) + this[Tables.Actions.oldObjectId] = getOrCreateRegistryKeyId(action.oldObjectIdentifier) + this[Tables.Actions.world] = getOrCreateWorldId(action.world ?: Ledger.server.overworld.registryKey.value) this[Tables.Actions.blockState] = action.objectState this[Tables.Actions.oldBlockState] = action.oldObjectState this[Tables.Actions.sourceName] = getOrCreateSourceId(action.sourceName) - this[Tables.Actions.sourcePlayer] = action.sourceProfile?.let { getPlayerId(it.id) } + this[Tables.Actions.sourcePlayer] = action.sourceProfile?.let { getOrCreatePlayerId(it.id) } this[Tables.Actions.extraData] = action.extraData } } @@ -440,7 +452,10 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { Tables.Actions.oldObjectId }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() @@ -479,7 +494,10 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { Tables.Actions.oldObjectId }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() @@ -497,13 +515,17 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { Tables.Actions.oldObjectId }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() .andWhere { buildQueryParams(params) and (Tables.Actions.rolledBack eq false) } .orderBy(Tables.Actions.id, SortOrder.DESC) - val actionIds = selectQuery.map { it[Tables.Actions.id] }.toSet() // SQLite doesn't support update where so select by ID. Might not be as efficent + val actionIds = selectQuery.map { it[Tables.Actions.id] } + .toSet() // SQLite doesn't support update where so select by ID. Might not be as efficent actions.addAll(getActionsFromQuery(selectQuery)) val updateQuery = Tables.Actions @@ -521,7 +543,10 @@ object DatabaseManager { .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) - .innerJoin(Tables.oldObjectTable, { Tables.Actions.oldObjectId }, { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) + .innerJoin( + Tables.oldObjectTable, + { Tables.Actions.oldObjectId }, + { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] }) .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() @@ -538,62 +563,122 @@ object DatabaseManager { return actions } - private fun getPlayerId(playerId: UUID): Int? { - cache.playerKeys.getIfPresent(playerId)?.let { return it } + fun getKnownSources() = + cache.sourceKeys.asMap().keys - return Tables.Player.find { Tables.Players.playerId eq playerId }.firstOrNull()?.id?.value?.also { - cache.playerKeys.put(playerId, it) + private fun getObjectId( + obj: T, + cache: Cache, + table: EntityClass>, + column: Column + ): Int? = getObjectId(obj, Function.identity(), cache, table, column) + + private fun getObjectId( + obj: T, + mapper: Function, + cache: Cache, + table: EntityClass>, + column: Column + ): Int? { + cache.getIfPresent(obj)?.let { return it } + return table.find { column eq mapper.apply(obj) }.firstOrNull()?.id?.value?.also { + cache.put(obj, it) } } - private fun Transaction.selectPlayer(playerName: String) = - Tables.Player.find { Tables.Players.playerName.lowerCase() eq playerName }.firstOrNull() - - fun getKnownSources() = - cache.sourceKeys.asMap().keys + private fun getOrCreateObjectId( + obj: T, + cache: Cache, + entity: IntEntityClass<*>, + table: IntIdTable, + column: Column + ): Int = + getOrCreateObjectId(obj, Function.identity(), cache, entity, table, column) + + private fun getOrCreateObjectId( + obj: T, + mapper: Function, + cache: Cache, + entity: IntEntityClass<*>, + table: IntIdTable, + column: Column + ): Int { + getObjectId(obj, mapper, cache, entity, column)?.let { return it } + + return entity[ + table.insertAndGetId { + it[column] = mapper.apply(obj) + } + ].id.value.also { cache.put(obj, it) } + } - private fun getSourceId(source: String): Int? { - cache.sourceKeys.getIfPresent(source)?.let { return it } + private fun getOrCreatePlayerId(playerId: UUID): Int = + getOrCreateObjectId(playerId, cache.playerKeys, Tables.Player, Tables.Players, Tables.Players.playerId) - return Tables.Source.find { Tables.Sources.name eq source }.firstOrNull()?.id?.value.also { - it?.let { cache.sourceKeys.put(source, it) } - } - } + private fun getOrCreateSourceId(source: String): Int = + getOrCreateObjectId(source, cache.sourceKeys, Tables.Source, Tables.Sources, Tables.Sources.name) - private fun getOrCreateSourceId(source: String): Int { - cache.sourceKeys.getIfPresent(source)?.let { return it } - Tables.Source.find { Tables.Sources.name eq source }.firstOrNull()?.let { return it.id.value } + private fun getOrCreateActionId(actionTypeId: String): Int = + getOrCreateObjectId( + actionTypeId, + cache.actionIdentifierKeys, + Tables.ActionIdentifier, + Tables.ActionIdentifiers, + Tables.ActionIdentifiers.actionIdentifier + ) - return Tables.Source[ - Tables.Sources.insertAndGetId { - it[name] = source - } - ].id.value.also { cache.sourceKeys.put(source, it) } - } + private fun getOrCreateRegistryKeyId(identifier: Identifier): Int = + getOrCreateObjectId( + identifier, + Identifier::toString, + cache.objectIdentifierKeys, + Tables.ObjectIdentifier, + Tables.ObjectIdentifiers, + Tables.ObjectIdentifiers.identifier + ) - private fun getActionId(actionTypeId: String): Int { - cache.actionIdentifierKeys.getIfPresent(actionTypeId)?.let { return it } + private fun getOrCreateWorldId(identifier: Identifier): Int = + getOrCreateObjectId( + identifier, + Identifier::toString, + cache.worldIdentifierKeys, + Tables.World, + Tables.Worlds, + Tables.Worlds.identifier + ) - return Tables.ActionIdentifier.find { Tables.ActionIdentifiers.actionIdentifier eq actionTypeId } - .first().id.value - .also { cache.actionIdentifierKeys.put(actionTypeId, it) } - } + private fun getPlayerId(playerId: UUID): Int? = + getObjectId(playerId, cache.playerKeys, Tables.Player, Tables.Players.playerId) - private fun getRegistryKeyId(identifier: Identifier): Int { - cache.objectIdentifierKeys.getIfPresent(identifier)?.let { return it } + private fun getSourceId(source: String): Int? = + getObjectId(source, cache.sourceKeys, Tables.Source, Tables.Sources.name) - return Tables.ObjectIdentifier.find { Tables.ObjectIdentifiers.identifier eq identifier.toString() } - .limit(1).first().id.value - .also { cache.objectIdentifierKeys.put(identifier, it) } - } + private fun getActionId(actionTypeId: String): Int? = + getObjectId( + actionTypeId, + cache.actionIdentifierKeys, + Tables.ActionIdentifier, + Tables.ActionIdentifiers.actionIdentifier + ) - private fun getWorldId(identifier: Identifier): Int { - cache.worldIdentifierKeys.getIfPresent(identifier)?.let { return it } + private fun getRegistryKeyId(identifier: Identifier): Int? = + getObjectId( + identifier, + Identifier::toString, + cache.objectIdentifierKeys, + Tables.ObjectIdentifier, + Tables.ObjectIdentifiers.identifier + ) - return Tables.World.find { Tables.Worlds.identifier eq identifier.toString() }.limit(1).first().id.value - .also { cache.worldIdentifierKeys.put(identifier, it) } - } + private fun getWorldId(identifier: Identifier): Int? = + getObjectId( + identifier, + Identifier::toString, + cache.worldIdentifierKeys, + Tables.World, + Tables.Worlds.identifier + ) // Workaround because can't delete from a join in exposed https://kotlinlang.slack.com/archives/C0CG7E0A1/p1605866974117400 private fun Transaction.purgeActions(params: ActionSearchParams) = Tables.Actions From 5041b93380856fd7cf77ef7bc255faef6202a774 Mon Sep 17 00:00:00 2001 From: Tim Kolijn Date: Thu, 30 May 2024 00:12:35 +0200 Subject: [PATCH 2/4] Update install.md (#259) --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 1034f412..91a40f72 100644 --- a/docs/install.md +++ b/docs/install.md @@ -40,7 +40,7 @@ properties = [] `url`: Must be URL of database with `/` appended. An example URL would be `localhost/ledger`. You can optionally add port information such as `localhost:3000/ledger` ### PostgreSQL -MySQL requires running a separate PostgreSQL database and more setup than just plug and play SQLite, but can support much larger databases at faster speeds. It is more experimental the MySQL but may yield faster performance. +PostgreSQL requires running a separate PostgreSQL database and more setup than just plug and play SQLite, but can support much larger databases at faster speeds. It is more experimental the MySQL but may yield faster performance. Add the following to the bottom of your Ledger config file: From 7cc3337efc3a970fa515bf1a4ac1acb794ef65bf Mon Sep 17 00:00:00 2001 From: matisanabria <143863247+matisanabria@users.noreply.github.com> Date: Wed, 29 May 2024 18:17:43 -0400 Subject: [PATCH 3/4] Spanish translation for Ledger (#263) --- .../resources/data/ledger/lang/es_es.json | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/main/resources/data/ledger/lang/es_es.json diff --git a/src/main/resources/data/ledger/lang/es_es.json b/src/main/resources/data/ledger/lang/es_es.json new file mode 100644 index 00000000..07b45447 --- /dev/null +++ b/src/main/resources/data/ledger/lang/es_es.json @@ -0,0 +1,66 @@ +{ + "text.ledger.header.search.pos": "--- Buscando registros en %s ---", + "text.ledger.header.search": "------ Buscando registros ------", + "text.ledger.header.status": "------ Ledger ------", + + "text.ledger.footer.search": "------- %s [Página %s de %s] %s -------", + "text.ledger.footer.page_forward": ">>", + "text.ledger.footer.page_forward.hover": "Página siguiente", + "text.ledger.footer.page_backward": "<<", + "text.ledger.footer.page_backward.hover": "Página anterior", + + "text.ledger.status.queue": "Cola de Base de Datos: %s", + "text.ledger.status.queue.busy": "Ocupada", + "text.ledger.status.queue.empty": "Vacía", + "text.ledger.status.version": "Versión: %s", + "text.ledger.status.discord": "Discord: %s", + "text.ledger.status.discord.join": "Clic para unirse", + "text.ledger.status.db_type": "Tipo de Base de Datos: %s", + "text.ledger.status.wiki": "Wiki: %s", + "text.ledger.status.wiki.view": "Clic para ver", + + "text.ledger.preview.start": "Iniciando vista previa", + + "text.ledger.action_message": "%1$s %2$s %3$s %4$s %5$s", + "text.ledger.action_message.time_diff": "hace %s", + "text.ledger.action_message.location.hover": "Clic para teletransportarse", + "text.ledger.action_message.with": "with", + + "text.ledger.rollback.start": "Retrocediendo %s acciones", + "text.ledger.rollback.finish": "Retroceso completo", + "text.ledger.rollback.fail": "Fallo al retroceder %s x%s", + + "text.ledger.restore.start": "Restaurando %s acciones", + "text.ledger.restore.finish": "Restauración completa", + "text.ledger.restore.fail": "Fallo al restaurar %s x%s", + + "text.ledger.inspect.toggle": "Modo de inspección: %s", + "text.ledger.inspect.on": "Activado", + "text.ledger.inspect.off": "Desactivado", + + "text.ledger.database.busy": "La base de datos está actualmente ocupada, esto puede tomar un momento.", + + "error.ledger.no_cached_params": "Sin búsqueda en caché, realice una búsqueda antes de continuar.", + "error.ledger.command.no_results": "Sin resultados de búsqueda", + "error.ledger.no_more_pages": "No más páginas", + "error.ledger.unknown_param": "Parámetro desconocido %s", + "error.ledger.no_preview": "Sin vista previa", + + "text.ledger.action.block-place": "colocado", + "text.ledger.action.block-break": "roto", + "text.ledger.action.block-change": "cambiado", + "text.ledger.action.item-insert": "añadido", + "text.ledger.action.item-remove": "quitado", + "text.ledger.action.item-pick-up": "recogido", + "text.ledger.action.item-drop": "dropeado", + "text.ledger.action.entity-kill": "matado", + "text.ledger.action.entity-change": "cambiado", + + "text.ledger.network.protocols_mismatched": "Las versiones del protocolo no coinciden: la versión del Ledger es %d y la versión del cliente es %d", + "text.ledger.network.no_mod_info": "No se pudo identificar la información del mod de cliente.", + + "text.ledger.purge.starting": "------ Iniciando purga ------", + "text.ledger.purge.complete": "------ Purga completada ------", + + "text.ledger.player.result": "%1$s: Primera conexión: %2$s. Última conexión: %3$s" +} \ No newline at end of file From d62c25fb4a61a5d6fcd90d7a0e0a3c95bbf228ee Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 29 May 2024 18:07:27 -0700 Subject: [PATCH 4/4] Custom database file locations (#264) --- docs/config.md | 15 ++++++++++++++- .../ledger/api/ExtensionManager.kt | 4 ++-- .../ledger/config/DatabaseSpec.kt | 14 ++++++++++++++ .../ledger/database/DatabaseManager.kt | 4 ++-- src/main/resources/ledger.toml | 2 ++ 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/config.md b/docs/config.md index 539baeb9..279d4eb3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -14,7 +14,20 @@ Found under `[database]` `queueTimeoutMin` [Default: 5] is the maximum amount of time to wait for the queue to drain when the server stops in minutes -`queueCheckDelaySec` [Default: 10] is the amount of time between checking if the queue is empty when the server stops in seconds +`queueCheckDelaySec` [Default: 10] is the frequency in seconds to notify in console that the queue is not empty when the server stops + +`autoPurgeDays` [Default: -1] is the number of days to keep actions in the database. If set to -1, actions will never be purged automatically + +`batchSize` [Default: 1000] is the number of actions to insert into the database at once. +This can be increased to improve performance, but may cause issues with slow databases + +`batchDelay` [Default: 10] is the amount of time in ticks to wait between batches if the next batch isn't full. +This can be increased to improve performance, but may cause issues with slow databases + +`location` [Default: Nothing] is the location of the database file when using the default SQLite database or other file based databases like H2. +The path is relative to the server's root directory. If the path is left out, the database will default to the server's world directory. + +`logSQL` [Default: false] will log all SQL queries to the console. This is useful for debugging, but can be very spammy ### Search settings diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt b/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt index ee59f709..e676e127 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt @@ -2,8 +2,8 @@ package com.github.quiltservertools.ledger.api import com.github.quiltservertools.ledger.Ledger import com.github.quiltservertools.ledger.config.config +import com.github.quiltservertools.ledger.config.getDatabasePath import net.minecraft.server.MinecraftServer -import net.minecraft.util.WorldSavePath import javax.sql.DataSource object ExtensionManager { @@ -30,7 +30,7 @@ object ExtensionManager { extensions.forEach { if (it is DatabaseExtension) { if (dataSource == null) { - dataSource = it.getDataSource(server.getSavePath(WorldSavePath.ROOT)) + dataSource = it.getDataSource(config.getDatabasePath()) } else { failExtensionRegistration(it) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt b/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt index 8314ab96..62e953f2 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt @@ -1,6 +1,10 @@ package com.github.quiltservertools.ledger.config +import com.github.quiltservertools.ledger.Ledger +import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec +import net.minecraft.util.WorldSavePath +import java.nio.file.Path @Suppress("MagicNumber") object DatabaseSpec : ConfigSpec() { @@ -10,4 +14,14 @@ object DatabaseSpec : ConfigSpec() { val batchSize by optional(1000) val batchDelay by optional(10) val logSQL by optional(false) + val location by optional(null) +} + +fun Config.getDatabasePath(): Path { + val location = config[DatabaseSpec.location] + return if (location != null) { + Path.of(location) + } else { + Ledger.server.getSavePath(WorldSavePath.ROOT) + } } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt index 1eb647c9..13cf4c7d 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt @@ -8,6 +8,7 @@ import com.github.quiltservertools.ledger.actionutils.SearchResults import com.github.quiltservertools.ledger.config.DatabaseSpec import com.github.quiltservertools.ledger.config.SearchSpec import com.github.quiltservertools.ledger.config.config +import com.github.quiltservertools.ledger.config.getDatabasePath import com.github.quiltservertools.ledger.logInfo import com.github.quiltservertools.ledger.logWarn import com.github.quiltservertools.ledger.registry.ActionRegistry @@ -17,7 +18,6 @@ import com.google.common.cache.Cache import com.mojang.authlib.GameProfile import kotlinx.coroutines.delay import net.minecraft.util.Identifier -import net.minecraft.util.WorldSavePath import net.minecraft.util.math.BlockPos import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.EntityClass @@ -81,7 +81,7 @@ object DatabaseManager { } private fun getDefaultDatasource(): DataSource { - val dbFilepath = Ledger.server.getSavePath(WorldSavePath.ROOT).resolve("ledger.sqlite").pathString + val dbFilepath = config.getDatabasePath().resolve("ledger.sqlite").pathString return SQLiteDataSource(SQLiteConfig().apply { setJournalMode(SQLiteConfig.JournalMode.WAL) }).apply { diff --git a/src/main/resources/ledger.toml b/src/main/resources/ledger.toml index b04e48a8..eb32917d 100644 --- a/src/main/resources/ledger.toml +++ b/src/main/resources/ledger.toml @@ -9,6 +9,8 @@ autoPurgeDays = -1 batchSize = 1000 # The amount of time to wait between batches in ticks (20 ticks = 1 second) batchDelay = 10 +# The location of the database file. Defaults to the world folder if not specified +#location = "./custom-dir" [search] # Number of actions to show per page