diff --git a/build.gradle.kts b/build.gradle.kts index b6ba8c6a..ccde0122 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - kotlin("jvm") version "1.7.10" - id("fabric-loom") version "1.4.+" - id("maven-publish") - id("io.gitlab.arturbosch.detekt") version "1.19.0" - id("com.github.jakemarsden.git-hooks") version "0.0.2" - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.kotlin) + alias(libs.plugins.loom) + alias(libs.plugins.detekt) + alias(libs.plugins.git.hooks) + alias(libs.plugins.shadow) + `maven-publish` } val props = properties @@ -80,7 +80,7 @@ dependencies { shadow(libs.exposed.jdbc) shadow(libs.exposed.java.time) shadow(libs.sqlite.jdbc) - + // Config shadow(libs.konf.core) shadow(libs.konf.toml) @@ -154,7 +154,6 @@ tasks { val relocPath = "com.github.quiltservertools.libs." relocate("com.fasterxml", relocPath + "com.fasterxml") relocate("com.moandjiezana.toml", relocPath + "com.moandjiezana.toml") - relocate("com.uchuhimo.konf", relocPath + "com.uchuhimo.konf") relocate("javassist", relocPath + "javassist") // Relocate each apache lib separately as just org.apache.commons will relocate things that aren't shadowed and break stuff relocate("org.apache.commons.lang3", relocPath + "org.apache.commons.lang3") diff --git a/docs/api/using_the_database.md b/docs/api/using_the_database.md index 82fd125a..1d2650f2 100644 --- a/docs/api/using_the_database.md +++ b/docs/api/using_the_database.md @@ -40,10 +40,8 @@ object LedgerExamples { entity: BlockEntity?) { // Create action object val action = ActionFactory.blockPlaceAction(world, pos, state, player, entity) - Ledger.launch { - // Insert action into the database - DatabaseManager.logAction(action) - } + // Insert action into the database + ActionQueueService.addToQueue(action) } } ``` diff --git a/gradle.properties b/gradle.properties index 3e8f04b7..d771a068 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ kotlin.code.style=official -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx2G # Mod Properties -modVersion = 1.2.10 +modVersion = 1.3.0 mavenGroup = com.github.quiltservertools modId = ledger modName = Ledger diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c0..7454180f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew index 4f906e0c..c53aefaa 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/libs.versions.toml b/libs.versions.toml index 89646967..433ff829 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -3,18 +3,18 @@ minecraft = "1.20.4" yarn-mappings = "1.20.4+build.1" fabric-loader = "0.15.1" -fabric-api = "0.91.2+1.20.4" +fabric-api = "0.91.3+1.20.4" # Kotlin -kotlin = "1.9.4" +kotlin = "1.8.22" # Also modrinth version in gradle.properties fabric-kotlin = "1.9.4+kotlin.1.8.21" fabric-permissions = "0.3-SNAPSHOT" translations = "2.2.0+1.20.3-rc1" -exposed = "0.38.2" -sqlite-jdbc = "3.36.0.3" +exposed = "0.46.0" +sqlite-jdbc = "3.44.1.0" konf = "1.1.2" @@ -42,3 +42,9 @@ konf-toml = { module = "com.uchuhimo:konf-toml", version.ref = "konf"} wdmcf = { module = "me.bymartrixx:wdmcf", version.ref = "wdmcf" } +[plugins] +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.19.0" } +loom = { id = "fabric-loom", version = "1.4.+" } +git_hooks = { id = "com.github.jakemarsden.git-hooks", version = "0.0.2" } +shadow = { id = "com.github.johnrengelman.shadow", version = "7.1.2" } \ No newline at end of file diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/Ledger.kt b/src/main/kotlin/com/github/quiltservertools/ledger/Ledger.kt index a8871d48..e06afa3d 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/Ledger.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/Ledger.kt @@ -2,11 +2,13 @@ package com.github.quiltservertools.ledger import com.github.quiltservertools.ledger.actionutils.ActionSearchParams import com.github.quiltservertools.ledger.actionutils.Preview +import com.github.quiltservertools.ledger.api.ExtensionManager import com.github.quiltservertools.ledger.api.LedgerApi import com.github.quiltservertools.ledger.api.LedgerApiImpl import com.github.quiltservertools.ledger.commands.registerCommands import com.github.quiltservertools.ledger.config.CONFIG_PATH import com.github.quiltservertools.ledger.config.DatabaseSpec +import com.github.quiltservertools.ledger.database.ActionQueueService import com.github.quiltservertools.ledger.database.DatabaseManager import com.github.quiltservertools.ledger.listeners.registerBlockListeners import com.github.quiltservertools.ledger.listeners.registerEntityListeners @@ -15,7 +17,13 @@ import com.github.quiltservertools.ledger.listeners.registerWorldEventListeners import com.github.quiltservertools.ledger.network.Networking import com.github.quiltservertools.ledger.registry.ActionRegistry import com.uchuhimo.konf.Config -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import net.fabricmc.api.DedicatedServerModInitializer import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents @@ -23,21 +31,20 @@ import net.fabricmc.loader.api.FabricLoader import net.minecraft.registry.Registries import net.minecraft.server.MinecraftServer import net.minecraft.util.Identifier -import net.minecraft.util.WorldSavePath import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger +import org.jetbrains.exposed.sql.vendors.SQLiteDialect import java.nio.file.Files -import java.util.* +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime import com.github.quiltservertools.ledger.config.config as realConfig object Ledger : DedicatedServerModInitializer, CoroutineScope { const val MOD_ID = "ledger" - const val DEFAULT_DATABASE = "sqlite" + val DEFAULT_DATABASE = SQLiteDialect.dialectName @JvmStatic val api: LedgerApi = LedgerApiImpl @@ -71,33 +78,49 @@ object Ledger : DedicatedServerModInitializer, CoroutineScope { private fun serverStarting(server: MinecraftServer) { this.server = server - DatabaseManager.setValues(server.getSavePath(WorldSavePath.ROOT).resolve("ledger.sqlite").toFile(), server) + ExtensionManager.serverStarting(server) + DatabaseManager.setup(ExtensionManager.getDataSource()) DatabaseManager.ensureTables() - DatabaseManager.autoPurge() + ActionRegistry.registerDefaultTypes() initListeners() Networking - val idSet = setOf() - .plus(Registries.BLOCK.ids) - .plus(Registries.ITEM.ids) - .plus(Registries.ENTITY_TYPE.ids) - Ledger.launch { + val idSet = setOf() + .plus(Registries.BLOCK.ids) + .plus(Registries.ITEM.ids) + .plus(Registries.ENTITY_TYPE.ids) + logInfo("Inserting ${idSet.size} registry keys into the database...") DatabaseManager.insertIdentifiers(idSet) logInfo("Registry insert complete") + + DatabaseManager.setupCache() + DatabaseManager.autoPurge() + }.invokeOnCompletion { + ActionQueueService.start() } } - @OptIn(ExperimentalTime::class) private fun serverStopped(server: MinecraftServer) { runBlocking { - withTimeout(config[DatabaseSpec.queueTimeoutMin].minutes) { - while (DatabaseManager.dbMutex.isLocked) { - logInfo("Database queue is still draining. If you exit now actions WILL be lost") - delay(config[DatabaseSpec.queueCheckDelaySec].seconds) + try { + withTimeout(config[DatabaseSpec.queueTimeoutMin].minutes) { + Ledger.launch(Dispatchers.Default) { + while (ActionQueueService.size > 0) { + logInfo( + "Database is still busy. If you exit now data WILL be lost. Actions in queue: ${ActionQueueService.size}" + ) + + delay(config[DatabaseSpec.queueCheckDelaySec].seconds) + } + } + ActionQueueService.drainAll() + logInfo("Successfully drained database queue") } + } catch (e: TimeoutCancellationException) { + logWarn("Database drain timed out. ${ActionQueueService.size} actions still in queue. Data may be lost.") } } } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/actionutils/ActionSearchParams.kt b/src/main/kotlin/com/github/quiltservertools/ledger/actionutils/ActionSearchParams.kt index 20489e17..27d5cc01 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/actionutils/ActionSearchParams.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/actionutils/ActionSearchParams.kt @@ -4,6 +4,7 @@ import com.github.quiltservertools.ledger.utility.Negatable import net.minecraft.util.Identifier import net.minecraft.util.math.BlockBox import java.time.Instant +import java.util.UUID data class ActionSearchParams( val bounds: BlockBox?, @@ -12,7 +13,7 @@ data class ActionSearchParams( var actions: MutableSet>?, var objects: MutableSet>?, var sourceNames: MutableSet>?, - var sourcePlayerNames: MutableSet>?, + var sourcePlayerIds: MutableSet>?, var worlds: MutableSet>?, ) { private constructor(builder: Builder) : this( @@ -22,11 +23,11 @@ data class ActionSearchParams( builder.actions, builder.objects, builder.sourceNames, - builder.sourcePlayerNames, + builder.sourcePlayerIds, builder.worlds ) - fun isEmpty() = listOf(bounds, before, after, actions, objects, sourceNames, sourcePlayerNames, worlds).all { it == null } + fun isEmpty() = listOf(bounds, before, after, actions, objects, sourceNames, sourcePlayerIds, worlds).all { it == null } companion object { inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build() @@ -39,7 +40,7 @@ data class ActionSearchParams( var actions: MutableSet>? = null var objects: MutableSet>? = null var sourceNames: MutableSet>? = null - var sourcePlayerNames: MutableSet>? = null + var sourcePlayerIds: MutableSet>? = null var worlds: MutableSet>? = null fun build() = ActionSearchParams(this) diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/api/DatabaseExtension.kt b/src/main/kotlin/com/github/quiltservertools/ledger/api/DatabaseExtension.kt index 7da95d29..3e2242cf 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/api/DatabaseExtension.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/api/DatabaseExtension.kt @@ -1,8 +1,8 @@ package com.github.quiltservertools.ledger.api -import net.minecraft.server.MinecraftServer -import org.jetbrains.exposed.sql.Database +import java.nio.file.Path +import javax.sql.DataSource interface DatabaseExtension : LedgerExtension { - fun getDatabase(server: MinecraftServer): Database + fun getDataSource(savePath: Path): DataSource } 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 dfcf73ec..ee59f709 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/api/ExtensionManager.kt @@ -2,38 +2,45 @@ package com.github.quiltservertools.ledger.api import com.github.quiltservertools.ledger.Ledger import com.github.quiltservertools.ledger.config.config -import java.util.* +import net.minecraft.server.MinecraftServer +import net.minecraft.util.WorldSavePath +import javax.sql.DataSource object ExtensionManager { - private val extensions = mutableListOf() + private val _extensions = mutableListOf() + val extensions: List + get() = _extensions - private var databaseExtension: Optional = Optional.empty() + private var dataSource: DataSource? = null val commands = mutableListOf() fun registerExtension(extension: LedgerExtension) { - extensions.add(extension) - - if (extension is DatabaseExtension) { - if(databaseExtension.isEmpty) { - databaseExtension = Optional.of(extension) - } else { - failExtensionRegistration(extension) - } - } + _extensions.add(extension) if (extension is CommandExtension) { commands.add(extension) } - extension.getConfigSpecs().forEach { config.addSpec(it) } } + internal fun serverStarting(server: MinecraftServer) { + extensions.forEach { + if (it is DatabaseExtension) { + if (dataSource == null) { + dataSource = it.getDataSource(server.getSavePath(WorldSavePath.ROOT)) + } else { + failExtensionRegistration(it) + } + } + } + } + private fun failExtensionRegistration(extension: LedgerExtension) { Ledger.logger.error("Unable to load extension ${extension.getIdentifier()}") } - fun getDatabaseExtensionOptional() = databaseExtension + fun getDataSource() = dataSource } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/api/LedgerApiImpl.kt b/src/main/kotlin/com/github/quiltservertools/ledger/api/LedgerApiImpl.kt index 8b073f34..d9162ccd 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/api/LedgerApiImpl.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/api/LedgerApiImpl.kt @@ -4,9 +4,9 @@ import com.github.quiltservertools.ledger.Ledger import com.github.quiltservertools.ledger.actions.ActionType import com.github.quiltservertools.ledger.actionutils.ActionSearchParams import com.github.quiltservertools.ledger.actionutils.SearchResults +import com.github.quiltservertools.ledger.database.ActionQueueService import com.github.quiltservertools.ledger.database.DatabaseManager import kotlinx.coroutines.future.future -import kotlinx.coroutines.launch import java.util.concurrent.CompletableFuture internal object LedgerApiImpl : LedgerApi { @@ -23,6 +23,6 @@ internal object LedgerApiImpl : LedgerApi { Ledger.future { DatabaseManager.restoreActions(params) } override fun logAction(action: ActionType) { - Ledger.launch { DatabaseManager.logAction(action) } + ActionQueueService.addToQueue(action) } } 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 b9b9bad9..6bea9a68 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 @@ -137,11 +137,17 @@ object SearchParamArgument { builder.sourceNames!!.add(nonPlayer) } } else { - if (builder.sourcePlayerNames == null) { - builder.sourcePlayerNames = - mutableSetOf(sourceInput) - } else { - builder.sourcePlayerNames!!.add(sourceInput) + val profile = source.server.userCache?.findByName(sourceInput.property) + val id = profile?.orElse(null)?.id + + if (id != null) { + val playerIdEntry = Negatable(id, sourceInput.allowed) + if (builder.sourcePlayerIds == null) { + builder.sourcePlayerIds = + mutableSetOf(playerIdEntry) + } else { + builder.sourcePlayerIds!!.add(playerIdEntry) + } } } } 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 626cd603..676b013b 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 @@ -1,12 +1,13 @@ package com.github.quiltservertools.ledger.commands.parameters +import com.github.quiltservertools.ledger.database.DatabaseManager 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,7 +28,9 @@ class SourceParameter : SimpleParameter() { stringReader.cursor = builder.start val players = context.source.playerNames - players.add("@") + DatabaseManager.getKnownSources().forEach { + players.add("@$it") + } // TODO suggest non-player sources return CommandSource.suggestMatching( diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/StatusCommand.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/StatusCommand.kt index 9abac86a..e734471d 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/StatusCommand.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/StatusCommand.kt @@ -1,11 +1,15 @@ package com.github.quiltservertools.ledger.commands.subcommands import com.github.quiltservertools.ledger.Ledger -import com.github.quiltservertools.ledger.api.ExtensionManager import com.github.quiltservertools.ledger.commands.BuildableCommand import com.github.quiltservertools.ledger.commands.CommandConsts +import com.github.quiltservertools.ledger.database.ActionQueueService import com.github.quiltservertools.ledger.database.DatabaseManager -import com.github.quiltservertools.ledger.utility.* +import com.github.quiltservertools.ledger.utility.Context +import com.github.quiltservertools.ledger.utility.LiteralNode +import com.github.quiltservertools.ledger.utility.TextColorPallet +import com.github.quiltservertools.ledger.utility.literal +import com.github.quiltservertools.ledger.utility.translate import kotlinx.coroutines.launch import me.lucko.fabric.api.permissions.v0.Permissions import net.fabricmc.loader.api.FabricLoader @@ -35,11 +39,8 @@ object StatusCommand : BuildableCommand { { Text.translatable( "text.ledger.status.queue", - if (DatabaseManager.dbMutex.isLocked) { - "text.ledger.status.queue.busy".translate().setStyle(TextColorPallet.secondaryVariant) - } else { - "text.ledger.status.queue.empty".translate().setStyle(TextColorPallet.secondaryVariant) - } + ActionQueueService.size.toString().literal() + .setStyle(TextColorPallet.secondaryVariant) ).setStyle(TextColorPallet.secondary) }, false @@ -54,16 +55,11 @@ object StatusCommand : BuildableCommand { }, false ) - val dbType = if (ExtensionManager.getDatabaseExtensionOptional().isPresent) { - ExtensionManager.getDatabaseExtensionOptional().get().getIdentifier() - } else { - Ledger.identifier(Ledger.DEFAULT_DATABASE) - } source.sendFeedback( { Text.translatable( "text.ledger.status.db_type", - dbType.path.literal() + DatabaseManager.databaseType.literal() .setStyle(TextColorPallet.secondaryVariant) ).setStyle(TextColorPallet.secondary) }, 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 19e9f9cb..8314ab96 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/config/DatabaseSpec.kt @@ -2,8 +2,12 @@ package com.github.quiltservertools.ledger.config import com.uchuhimo.konf.ConfigSpec +@Suppress("MagicNumber") object DatabaseSpec : ConfigSpec() { val queueTimeoutMin by required() val queueCheckDelaySec by required() val autoPurgeDays by required() + val batchSize by optional(1000) + val batchDelay by optional(10) + val logSQL by optional(false) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/ActionQueueService.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/ActionQueueService.kt new file mode 100644 index 00000000..556d2793 --- /dev/null +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/ActionQueueService.kt @@ -0,0 +1,53 @@ +package com.github.quiltservertools.ledger.database + +import com.github.quiltservertools.ledger.Ledger +import com.github.quiltservertools.ledger.actions.ActionType +import com.github.quiltservertools.ledger.config.DatabaseSpec +import com.github.quiltservertools.ledger.utility.ticks +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.LinkedBlockingQueue + +object ActionQueueService { + private val queue = LinkedBlockingQueue() + private lateinit var job: Job + + val size: Int get() = queue.size + + fun start() { + job = Ledger.launch { + prepareNextBatch() + } + } + + fun addToQueue(action: ActionType): Boolean { + if (action.isBlacklisted()) return false + + return queue.add(action) + } + + suspend fun drainAll() { + job.cancel() + while (queue.isNotEmpty()) { + drainBatch() + } + } + + private suspend fun drainBatch() { + val batch = mutableListOf() + queue.drainTo(batch, Ledger.config[DatabaseSpec.batchSize]) + + DatabaseManager.logActionBatch(batch) + } + + private suspend fun prepareNextBatch() { + job = Ledger.launch { + if (queue.size < Ledger.config[DatabaseSpec.batchSize]) { + delay(Ledger.config[DatabaseSpec.batchDelay].ticks) + } + drainBatch() + prepareNextBatch() + } + } +} diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseCacheService.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseCacheService.kt new file mode 100644 index 00000000..e8eb1edb --- /dev/null +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseCacheService.kt @@ -0,0 +1,18 @@ +package com.github.quiltservertools.ledger.database + +import com.google.common.cache.Cache +import com.google.common.cache.CacheBuilder +import net.minecraft.util.Identifier +import java.util.UUID + +object DatabaseCacheService { + val actionIdentifierKeys: Cache = CacheBuilder.newBuilder().build() + + val worldIdentifierKeys: Cache = CacheBuilder.newBuilder().build() + + val objectIdentifierKeys: Cache = CacheBuilder.newBuilder().build() + + val sourceKeys: Cache = CacheBuilder.newBuilder().build() + + val playerKeys: Cache = CacheBuilder.newBuilder().build() +} 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 97b65316..e8bca18d 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt @@ -5,7 +5,6 @@ import com.github.quiltservertools.ledger.actions.ActionType import com.github.quiltservertools.ledger.actionutils.ActionSearchParams import com.github.quiltservertools.ledger.actionutils.Preview import com.github.quiltservertools.ledger.actionutils.SearchResults -import com.github.quiltservertools.ledger.api.ExtensionManager import com.github.quiltservertools.ledger.config.DatabaseSpec import com.github.quiltservertools.ledger.config.SearchSpec import com.github.quiltservertools.ledger.config.config @@ -15,23 +14,27 @@ import com.github.quiltservertools.ledger.registry.ActionRegistry import com.github.quiltservertools.ledger.utility.Negatable import com.github.quiltservertools.ledger.utility.PlayerResult import com.mojang.authlib.GameProfile -import kotlinx.coroutines.channels.Channel +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* +import javax.sql.DataSource import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import net.minecraft.server.MinecraftServer import net.minecraft.util.Identifier +import net.minecraft.util.WorldSavePath import net.minecraft.util.math.BlockPos +import org.jetbrains.exposed.dao.id.EntityID 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.inSubQuery +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq +import org.jetbrains.exposed.sql.SqlLogger import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.alias import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere @@ -42,76 +45,86 @@ 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 +import org.jetbrains.exposed.sql.statements.StatementContext +import org.jetbrains.exposed.sql.statements.expandArgs import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.transaction -import java.io.File -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.* +import org.jetbrains.exposed.sql.update +import org.sqlite.SQLiteDataSource +import kotlin.io.path.pathString import kotlin.math.ceil object DatabaseManager { // These values are initialised late to allow the database to be created at server start, // which means the database file is located in the world folder and allows for per-world databases. - private lateinit var databaseFile: File private lateinit var database: Database - val dbMutex = Mutex() + val databaseType: String + get() = database.dialect.name - private val _actions = MutableSharedFlow(extraBufferCapacity = Channel.UNLIMITED) - val actions = _actions.asSharedFlow() + private val cache = DatabaseCacheService + private val dbMutex = Mutex() + private var enforceMutex = false - init { - Ledger.launch { - actions.collect { - try { - execute { - insertAction(it) - } - } catch (@Suppress("TooGenericExceptionCaught") e: Throwable) { - logWarn("Exception occurred while attempting to commit action. Skipping.", e) - } - } - } + fun setup(dataSource: DataSource?) { + val source = dataSource ?: getDefaultDatasource() + database = Database.connect(source) } - fun setValues(file: File, server: MinecraftServer) { - if (ExtensionManager.getDatabaseExtensionOptional().isPresent) { - // Extension present, load database from it - database = ExtensionManager.getDatabaseExtensionOptional().get().getDatabase(server) - } else { - // No database extension is present, load normally - databaseFile = file - database = Database.connect( - url = "jdbc:sqlite:${databaseFile.path.replace('\\', '/')}", - ) + private fun getDefaultDatasource(): DataSource { + val dbFilepath = Ledger.server.getSavePath(WorldSavePath.ROOT).resolve("ledger.sqlite").pathString + enforceMutex = true + return SQLiteDataSource().apply { + url = "jdbc:sqlite:$dbFilepath" } } fun ensureTables() = transaction { + addLogger(object : SqlLogger { + override fun log(context: StatementContext, transaction: Transaction) { + Ledger.logger.info("SQL: ${context.expandArgs(transaction)}") + } + }) SchemaUtils.createMissingTablesAndColumns( Tables.Players, Tables.Actions, Tables.ActionIdentifiers, Tables.ObjectIdentifiers, Tables.Sources, - Tables.Worlds + Tables.Worlds, + withLogs = true ) logInfo("Tables created") } - fun autoPurge() { + suspend fun setupCache() { + execute { + Tables.ActionIdentifier.all().forEach { + cache.actionIdentifierKeys.put(it.identifier, it.id.value) + } + Tables.World.all().forEach { + cache.worldIdentifierKeys.put(it.identifier, it.id.value) + } + Tables.ObjectIdentifier.all().forEach { + cache.objectIdentifierKeys.put(it.identifier, it.id.value) + } + Tables.Source.all().forEach { + cache.sourceKeys.put(it.name, it.id.value) + } + } + } + + suspend fun autoPurge() { if (config[DatabaseSpec.autoPurgeDays] > 0) { - Ledger.launch { - execute { - Ledger.logger.info("Purging actions older than ${config[DatabaseSpec.autoPurgeDays]} days") - val deleted = Tables.Actions.deleteWhere { - Tables.Actions.timestamp lessEq Instant.now() - .minus(config[DatabaseSpec.autoPurgeDays].toLong(), ChronoUnit.DAYS) - } - Ledger.logger.info("Successfully purged $deleted actions") + execute { + Ledger.logger.info("Purging actions older than ${config[DatabaseSpec.autoPurgeDays]} days") + val deleted = Tables.Actions.deleteWhere { + Tables.Actions.timestamp lessEq Instant.now() + .minus(config[DatabaseSpec.autoPurgeDays].toLong(), ChronoUnit.DAYS) } + Ledger.logger.info("Successfully purged $deleted actions") } } } @@ -125,11 +138,11 @@ object DatabaseManager { } suspend fun rollbackActions(params: ActionSearchParams): List = execute { - return@execute selectRollbackActions(params) + return@execute selectAndRollbackActions(params) } suspend fun restoreActions(params: ActionSearchParams): List = execute { - return@execute selectRestoreActions(params) + return@execute selectAndRestoreActions(params) } suspend fun previewActions( @@ -139,119 +152,126 @@ object DatabaseManager { return@execute selectActionsPreview(params, type) } - private fun daoToActionType(actions: List): List { - val actionTypes = mutableListOf() + private fun getActionsFromQuery(query: Query): List { + val actions = mutableListOf() - for (action in actions) { - val typeSupplier = ActionRegistry.getType(action.actionIdentifier.identifier) + for (action in query) { + val typeSupplier = ActionRegistry.getType(action[Tables.ActionIdentifiers.actionIdentifier]) if (typeSupplier == null) { - logWarn("Unknown action type ${action.actionIdentifier.identifier}") + logWarn("Unknown action type ${action[Tables.ActionIdentifiers.actionIdentifier]}") continue } val type = typeSupplier.get() - type.timestamp = action.timestamp - type.pos = BlockPos(action.x, action.y, action.z) - type.world = action.world.identifier - type.objectIdentifier = action.objectId.identifier - type.oldObjectIdentifier = action.oldObjectId.identifier - type.objectState = action.blockState - type.oldObjectState = action.oldBlockState - type.sourceName = action.sourceName.name - type.sourceProfile = action.sourcePlayer?.let { GameProfile(it.playerId, it.playerName) } - type.extraData = action.extraData - type.rolledBack = action.rolledBack - - actionTypes.add(type) + type.timestamp = action[Tables.Actions.timestamp] + type.pos = BlockPos(action[Tables.Actions.x], action[Tables.Actions.y], action[Tables.Actions.z]) + 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]] + ) + type.objectState = action[Tables.Actions.blockState] + type.oldObjectState = action[Tables.Actions.oldBlockState] + type.sourceName = action[Tables.Sources.name] + type.sourceProfile = action.getOrNull(Tables.Players.playerId)?.let { + GameProfile(it, action[Tables.Players.playerName]) + } + type.extraData = action[Tables.Actions.extraData] + type.rolledBack = action[Tables.Actions.rolledBack] + + actions.add(type) } - return actionTypes + return actions } - private fun buildQuery(params: ActionSearchParams): Query { - val oldObjectTable = Tables.ObjectIdentifiers.alias("oldObjects") - - val query = Tables.Actions - .innerJoin(Tables.ActionIdentifiers) - .innerJoin(Tables.Worlds) - .leftJoin(Tables.Players) - .innerJoin(oldObjectTable, { Tables.Actions.oldObjectId }, { oldObjectTable[Tables.ObjectIdentifiers.id] }) - .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) - .innerJoin(Tables.Sources) - .selectAll() + private fun buildQueryParams(params: ActionSearchParams): Op { + var op: Op = Op.TRUE if (params.bounds != null) { - query.andWhere { Tables.Actions.x.between(params.bounds.minX, params.bounds.maxX) } - query.andWhere { Tables.Actions.y.between(params.bounds.minY, params.bounds.maxY) } - query.andWhere { Tables.Actions.z.between(params.bounds.minZ, params.bounds.maxZ) } + op = op.and { Tables.Actions.x.between(params.bounds.minX, params.bounds.maxX) } + op = op.and { Tables.Actions.y.between(params.bounds.minY, params.bounds.maxY) } + op = op.and { Tables.Actions.z.between(params.bounds.minZ, params.bounds.maxZ) } } - if (params.before != null && params.after != null) { - query.andWhere { Tables.Actions.timestamp.greaterEq(params.after) } - .andWhere { Tables.Actions.timestamp.lessEq(params.before) } + op = op.and { + Tables.Actions.timestamp.greaterEq(params.after) and Tables.Actions.timestamp.lessEq(params.before) + } } else if (params.before != null) { - query.andWhere { Tables.Actions.timestamp.lessEq(params.before) } + op = op.and { Tables.Actions.timestamp.lessEq(params.before) } } else if (params.after != null) { - query.andWhere { Tables.Actions.timestamp.greaterEq(params.after) } + op = op.and { Tables.Actions.timestamp.greaterEq(params.after) } + } + + 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 + } } - addParameters( - query, - params.sourceNames, - Tables.Sources.name + op = addParameters( + op, + sourceNames, + Tables.Actions.sourceName ) - addParameters( - query, - params.actions, - Tables.ActionIdentifiers.actionIdentifier + op = addParameters( + op, + params.actions?.map { Negatable(getActionId(it.property), it.allowed) }, + Tables.Actions.actionIdentifier ) - addParameters( - query, - params.worlds?.map { - if (it.allowed) { - Negatable.allow(it.property.toString()) - } else { - Negatable.deny(it.property.toString()) - } - }, - Tables.Worlds.identifier + op = addParameters( + op, + params.worlds?.map { Negatable(getWorldId(it.property), it.allowed) }, + Tables.Actions.world ) - addParameters( - query, - params.objects?.map { - if (it.allowed) { - Negatable.allow(it.property.toString()) - } else { - Negatable.deny(it.property.toString()) - } - }, - Tables.ObjectIdentifiers.identifier, - oldObjectTable[Tables.ObjectIdentifiers.identifier] + op = addParameters( + op, + params.objects?.map { Negatable(getRegistryKeyId(it.property), it.allowed) }, + Tables.Actions.objectId, + Tables.Actions.oldObjectId ) - addParameters( - query, - params.sourcePlayerNames, - Tables.Players.playerName + op = addParameters( + op, + playerNames, + Tables.Actions.sourcePlayer ) - return query + return op } - private fun addParameters( - query: Query, + private fun , C : EntityID?> addParameters( + op: Op, paramSet: Collection>?, - column: Column, - orColumn: Column? = null - ) { + column: Column, + orColumn: Column? = null, + ): Op { fun addAllowedParameters( allowed: Collection, - ) { - if (allowed.isEmpty()) return + op: Op + ): Op { + if (allowed.isEmpty()) return op + var operator = if (orColumn != null) { Op.build { column eq allowed.first() or (orColumn eq allowed.first()) } @@ -267,13 +287,14 @@ object DatabaseManager { } } - query.andWhere { operator } + return op.and { operator } } fun addDeniedParameters( - denied: Collection - ) { - if (denied.isEmpty()) return + denied: Collection, + op: Op + ): Op { + if (denied.isEmpty()) return op var operator = if (orColumn != null) { Op.build { column neq denied.first() and (orColumn neq denied.first()) } @@ -289,19 +310,22 @@ object DatabaseManager { } } - query.andWhere { operator } + return op.and { operator } } - if (paramSet.isNullOrEmpty()) return + if (paramSet.isNullOrEmpty()) return op - addAllowedParameters(paramSet.filter { it.allowed }.map { it.property }) - addDeniedParameters(paramSet.filterNot { it.allowed }.map { it.property }) - } + var newOp = op + newOp = addAllowedParameters(paramSet.filter { it.allowed }.map { it.property }, newOp) + newOp = addDeniedParameters(paramSet.filterNot { it.allowed }.map { it.property }, newOp) - fun logAction(action: ActionType) { - if (action.isBlacklisted()) return + return newOp + } - _actions.tryEmit(action) + suspend fun logActionBatch(actions: List) { + execute { + insertActions(actions) + } } suspend fun registerWorld(identifier: Identifier) = @@ -316,7 +340,7 @@ object DatabaseManager { suspend fun logPlayer(uuid: UUID, name: String) = execute { - insertPlayer(uuid, name) + insertOrUpdatePlayer(uuid, name) } suspend fun insertIdentifiers(identifiers: Collection) = @@ -324,16 +348,23 @@ object DatabaseManager { insertRegKeys(identifiers) } - private suspend fun execute(body: suspend Transaction.() -> T): T = - dbMutex.withLock { - while (Ledger.server.overworld?.savingDisabled != false) { - delay(timeMillis = 1000) - } + private suspend fun execute(body: suspend Transaction.() -> T): T { + if (enforceMutex) dbMutex.lock() + while (Ledger.server.overworld?.savingDisabled != false) { + delay(timeMillis = 1000) + } - newSuspendedTransaction(db = database) { - body(this) + return newSuspendedTransaction(db = database) { + if (Ledger.config[DatabaseSpec.logSQL]) { + addLogger(object : SqlLogger { + override fun log(context: StatementContext, transaction: Transaction) { + Ledger.logger.info("SQL: ${context.expandArgs(transaction)}") + } + }) } - } + body(this) + }.also { if (enforceMutex) dbMutex.unlock() } + } suspend fun purgeActions(params: ActionSearchParams) { execute { @@ -347,10 +378,8 @@ object DatabaseManager { } private fun Transaction.insertActionType(id: String) { - if (Tables.ActionIdentifier.find { Tables.ActionIdentifiers.actionIdentifier eq id }.empty()) { - val actionIdentifier = Tables.ActionIdentifier.new { - identifier = id - } + Tables.ActionIdentifiers.insertIgnore { + it[actionIdentifier] = id } } @@ -366,25 +395,25 @@ object DatabaseManager { } } - private fun Transaction.insertAction(action: ActionType) { - Tables.Action.new { - actionIdentifier = selectActionId(action.identifier) - timestamp = action.timestamp - x = action.pos.x - y = action.pos.y - z = action.pos.z - objectId = selectRegistryKey(action.objectIdentifier) - oldObjectId = selectRegistryKey(action.oldObjectIdentifier) - world = selectWorld(action.world ?: Ledger.server.overworld.registryKey.value) - blockState = action.objectState - oldBlockState = action.oldObjectState - sourceName = insertAndSelectSource(action.sourceName) - sourcePlayer = action.sourceProfile?.let { selectPlayer(it.id) } - extraData = action.extraData + private fun Transaction.insertActions(actions: List) { + Tables.Actions.batchInsert(actions, shouldReturnGeneratedValues = false) { action -> + this[Tables.Actions.actionIdentifier] = getActionId(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.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.extraData] = action.extraData } } - private fun Transaction.insertPlayer(uuid: UUID, name: String) { + private fun Transaction.insertOrUpdatePlayer(uuid: UUID, name: String) { val player = Tables.Player.find { Tables.Players.playerId eq uuid }.firstOrNull() if (player != null) { @@ -399,123 +428,182 @@ object DatabaseManager { } private fun Transaction.selectActionsSearch(params: ActionSearchParams, page: Int): SearchResults { - val actionTypes = mutableListOf() - var totalActions: Long = 0 + val actions = mutableListOf() + var totalActions: Long - var query = buildQuery(params) + var query = Tables.Actions + .innerJoin(Tables.ActionIdentifiers) + .innerJoin(Tables.Worlds) + .leftJoin(Tables.Players) + .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) } - totalActions = query.copy().count() - if (totalActions == 0L) return SearchResults(actionTypes, params, page, 0) + totalActions = countActions(params) + if (totalActions == 0L) return SearchResults(actions, params, page, 0) query = query.orderBy(Tables.Actions.id, SortOrder.DESC) query = query.limit( config[SearchSpec.pageSize], (config[SearchSpec.pageSize] * (page - 1)).toLong() - ).withDistinct() - - val actions = Tables.Action.wrapRows(query).toList() + ) // TODO better pagination without offset - probably doesn't matter as most people stay on first few pages - actionTypes.addAll(daoToActionType(actions)) + actions.addAll(getActionsFromQuery(query)) val totalPages = ceil(totalActions.toDouble() / config[SearchSpec.pageSize].toDouble()).toInt() - return SearchResults(actionTypes, params, page, totalPages) + return SearchResults(actions, params, page, totalPages) } - private fun Transaction.countActions(params: ActionSearchParams): Long = buildQuery(params).copy().count() + private fun Transaction.countActions(params: ActionSearchParams): Long = Tables.Actions + .selectAll() + .andWhere { buildQueryParams(params) } + .count() private fun Transaction.selectActionsPreview( params: ActionSearchParams, type: Preview.Type ): MutableList { - val actionTypes = mutableListOf() + val actions = mutableListOf() val isRestore = type == Preview.Type.RESTORE - val query = buildQuery(params) - .andWhere { Tables.Actions.rolledBack eq isRestore } + val selectQuery = Tables.Actions + .innerJoin(Tables.ActionIdentifiers) + .innerJoin(Tables.Worlds) + .leftJoin(Tables.Players) + .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 isRestore) } .orderBy(Tables.Actions.id, if (isRestore) SortOrder.ASC else SortOrder.DESC) + actions.addAll(getActionsFromQuery(selectQuery)) - val actions = Tables.Action.wrapRows(query).toList() - actionTypes.addAll(daoToActionType(actions)) - - return actionTypes + return actions } - private fun Transaction.selectRollbackActions(params: ActionSearchParams): MutableList { - val actionTypes = mutableListOf() + private fun Transaction.selectAndRollbackActions(params: ActionSearchParams): MutableList { + val actions = mutableListOf() - val query = buildQuery(params) - .andWhere { Tables.Actions.rolledBack eq false } + val selectQuery = Tables.Actions + .innerJoin(Tables.ActionIdentifiers) + .innerJoin(Tables.Worlds) + .leftJoin(Tables.Players) + .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 + actions.addAll(getActionsFromQuery(selectQuery)) - val actions = Tables.Action.wrapRows(query).toList() - for (action in actions) { - action.rolledBack = true - } - - actionTypes.addAll(daoToActionType(actions)) + val updateQuery = Tables.Actions + .update({ Tables.Actions.id inList actionIds and (Tables.Actions.rolledBack eq false) }) { + it[rolledBack] = true + } - return actionTypes + return actions } - private fun Transaction.selectRestoreActions(params: ActionSearchParams): MutableList { - val actionTypes = mutableListOf() + private fun Transaction.selectAndRestoreActions(params: ActionSearchParams): MutableList { + val actions = mutableListOf() - val query = buildQuery(params) - .andWhere { Tables.Actions.rolledBack eq true } + val selectQuery = Tables.Actions + .innerJoin(Tables.ActionIdentifiers) + .innerJoin(Tables.Worlds) + .leftJoin(Tables.Players) + .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 true) } .orderBy(Tables.Actions.id, SortOrder.ASC) + val actionIds = selectQuery.map { it[Tables.Actions.id] }.toSet() + actions.addAll(getActionsFromQuery(selectQuery)) - val actions = Tables.Action.wrapRows(query).toList() - for (action in actions) { - action.rolledBack = false - } - - actionTypes.addAll(daoToActionType(actions)) + val updateQuery = Tables.Actions + .update({ Tables.Actions.id inList actionIds and (Tables.Actions.rolledBack eq true) }) { + it[rolledBack] = false + } - return actionTypes + return actions } - private fun Transaction.selectPlayer(playerId: UUID) = - Tables.Player.find { Tables.Players.playerId eq playerId }.firstOrNull() + private fun getPlayerId(playerId: UUID): Int? { + cache.playerKeys.getIfPresent(playerId)?.let { return it } + + return Tables.Player.find { Tables.Players.playerId eq playerId }.firstOrNull()?.id?.value?.also { + cache.playerKeys.put(playerId, it) + } + } private fun Transaction.selectPlayer(playerName: String) = Tables.Player.find { Tables.Players.playerName.lowerCase() eq playerName }.firstOrNull() - private fun Transaction.insertAndSelectSource(source: String): Tables.Source { - var sourceDAO = Tables.Source.find { Tables.Sources.name eq source }.firstOrNull() + fun getKnownSources() = + cache.sourceKeys.asMap().keys - if (sourceDAO == null) { - sourceDAO = Tables.Source[Tables.Sources.insertAndGetId { - it[name] = source - }] + private fun getSourceId(source: String): Int? { + cache.sourceKeys.getIfPresent(source)?.let { return it } + + 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 { + cache.sourceKeys.getIfPresent(source)?.let { return it } - return sourceDAO + Tables.Source.find { Tables.Sources.name eq source }.firstOrNull()?.let { return it.id.value } + + return Tables.Source[ + Tables.Sources.insertAndGetId { + it[name] = source + } + ].id.value.also { cache.sourceKeys.put(source, it) } } - private fun Transaction.selectActionId(id: String) = - Tables.ActionIdentifier.find { Tables.ActionIdentifiers.actionIdentifier eq id }.first() + private fun getActionId(actionTypeId: String): Int { + cache.actionIdentifierKeys.getIfPresent(actionTypeId)?.let { return it } - private fun Transaction.selectRegistryKey(identifier: Identifier) = - Tables.ObjectIdentifier.find { Tables.ObjectIdentifiers.identifier eq identifier.toString() }.limit(1).first() + return Tables.ActionIdentifier.find { Tables.ActionIdentifiers.actionIdentifier eq actionTypeId } + .first().id.value + .also { cache.actionIdentifierKeys.put(actionTypeId, it) } + } - private fun Transaction.selectWorld(identifier: Identifier) = - Tables.World.find { Tables.Worlds.identifier eq identifier.toString() }.limit(1).first() + private fun getRegistryKeyId(identifier: Identifier): Int { + cache.objectIdentifierKeys.getIfPresent(identifier)?.let { return it } - private fun Transaction.purgeActions(params: ActionSearchParams) { - val query = buildQuery(params) - val actions = Tables.Action.wrapRows(query).toList() - actions.forEach { action -> - action.delete() - } + return Tables.ObjectIdentifier.find { Tables.ObjectIdentifiers.identifier eq identifier.toString() } + .limit(1).first().id.value + .also { cache.objectIdentifierKeys.put(identifier, it) } } + private fun getWorldId(identifier: Identifier): Int { + cache.worldIdentifierKeys.getIfPresent(identifier)?.let { return it } + + return Tables.World.find { Tables.Worlds.identifier eq identifier.toString() }.limit(1).first().id.value + .also { cache.worldIdentifierKeys.put(identifier, it) } + } + + // 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 + .deleteWhere { + Tables.Actions.id inSubQuery Tables.Actions.select(Tables.Actions.id) + .where(buildQueryParams(params)) + } + private fun Transaction.selectPlayers(players: Set): List { val query = Tables.Players.selectAll() - - addParameters(query, players.map { Negatable.allow(it.id) }, Tables.Players.playerId) + for (player in players) { + query.orWhere { Tables.Players.playerId eq player.id } + } return Tables.Player.wrapRows(query).toList().map { PlayerResult.fromRow(it) } } + } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/Tables.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/Tables.kt index daadd188..13a0b9d0 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/database/Tables.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/Tables.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.dao.IntEntity 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.alias import org.jetbrains.exposed.sql.javatime.timestamp import java.time.Instant @@ -44,6 +45,8 @@ object Tables { val identifier = varchar("identifier", MAX_IDENTIFIER_LENGTH).uniqueIndex() } + public val oldObjectTable = ObjectIdentifiers.alias("oldObjects") + class ObjectIdentifier(id: EntityID) : IntEntity(id) { var identifier by ObjectIdentifiers.identifier.transform({ it.toString() }, { Identifier.tryParse(it)!! }) @@ -51,24 +54,25 @@ object Tables { } object Actions : IntIdTable("actions") { - val actionIdentifier = reference("action_id", ActionIdentifiers.id) + val actionIdentifier = reference("action_id", ActionIdentifiers.id).index() val timestamp = timestamp("time") val x = integer("x") val y = integer("y") val z = integer("z") val world = reference("world_id", Worlds.id) - val objectId = reference("object_id", ObjectIdentifiers.id) - val oldObjectId = reference("old_object_id", ObjectIdentifiers.id) + val objectId = reference("object_id", ObjectIdentifiers.id).index() + val oldObjectId = reference("old_object_id", ObjectIdentifiers.id).index() val blockState = text("block_state").nullable() val oldBlockState = text("old_block_state").nullable() - val sourceName = reference("source", Sources.id) - val sourcePlayer = optReference("player_id", Players.id) + val sourceName = reference("source", Sources.id).index() + val sourcePlayer = optReference("player_id", Players.id).index() val extraData = text("extra_data").nullable() val rolledBack = bool("rolled_back").clientDefault { false } init { index("actions_by_location", false, x, y, z, world) } + } class Action(id: EntityID) : IntEntity(id) { diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/BlockEventListener.kt b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/BlockEventListener.kt index 2e1b4036..63c208db 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/BlockEventListener.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/BlockEventListener.kt @@ -6,7 +6,7 @@ import com.github.quiltservertools.ledger.callbacks.BlockBreakCallback import com.github.quiltservertools.ledger.callbacks.BlockChangeCallback import com.github.quiltservertools.ledger.callbacks.BlockMeltCallback import com.github.quiltservertools.ledger.callbacks.BlockPlaceCallback -import com.github.quiltservertools.ledger.database.DatabaseManager +import com.github.quiltservertools.ledger.database.ActionQueueService import com.github.quiltservertools.ledger.utility.Sources import net.minecraft.block.AirBlock import net.minecraft.block.BlockState @@ -36,7 +36,7 @@ fun onBlockPlace( } else { ActionFactory.blockPlaceAction(world, pos, state, source, entity) } - DatabaseManager.logAction(action) + ActionQueueService.addToQueue(action) } fun onBlockBreak( @@ -53,7 +53,7 @@ fun onBlockBreak( ActionFactory.blockBreakAction(world, pos, state, source, entity) } - DatabaseManager.logAction(action) + ActionQueueService.addToQueue(action) } fun onBlockChange( @@ -66,17 +66,17 @@ fun onBlockChange( source: String, player: PlayerEntity? ) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.blockChangeAction(world, pos, oldState, newState, oldBlockEntity, source, player) ) } fun onMelt(world: World, pos: BlockPos, oldState: BlockState, newState: BlockState, entity: BlockEntity?) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.blockBreakAction(world, pos, oldState, Sources.MELT, entity) ) if (newState.block !is AirBlock) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.blockPlaceAction(world, pos, newState, Sources.MELT, entity) ) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/EntityCallbackListener.kt b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/EntityCallbackListener.kt index 2677ab16..3a13e7fd 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/EntityCallbackListener.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/EntityCallbackListener.kt @@ -3,7 +3,7 @@ package com.github.quiltservertools.ledger.listeners import com.github.quiltservertools.ledger.actionutils.ActionFactory import com.github.quiltservertools.ledger.callbacks.EntityKillCallback import com.github.quiltservertools.ledger.callbacks.EntityModifyCallback -import com.github.quiltservertools.ledger.database.DatabaseManager +import com.github.quiltservertools.ledger.database.ActionQueueService import net.minecraft.entity.Entity import net.minecraft.entity.damage.DamageSource import net.minecraft.item.ItemStack @@ -17,8 +17,13 @@ fun registerEntityListeners() { EntityModifyCallback.EVENT.register(::onModify) } -private fun onKill(world: World, pos: BlockPos, entity: Entity, source: DamageSource) { - DatabaseManager.logAction( +private fun onKill( + world: World, + pos: BlockPos, + entity: Entity, + source: DamageSource +) { + ActionQueueService.addToQueue( ActionFactory.entityKillAction(world, pos, entity, source) ) } @@ -32,7 +37,7 @@ private fun onModify( entityActor: Entity?, sourceType: String ) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.entityChangeAction(world, pos, oldEntityTags, entity, itemStack, entityActor, sourceType) ) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/PlayerEventListener.kt b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/PlayerEventListener.kt index e1159658..e37e8568 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/PlayerEventListener.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/PlayerEventListener.kt @@ -4,6 +4,7 @@ import com.github.quiltservertools.ledger.Ledger import com.github.quiltservertools.ledger.actionutils.ActionFactory import com.github.quiltservertools.ledger.callbacks.ItemDropCallback import com.github.quiltservertools.ledger.callbacks.ItemPickUpCallback +import com.github.quiltservertools.ledger.database.ActionQueueService import com.github.quiltservertools.ledger.database.DatabaseManager import com.github.quiltservertools.ledger.network.Networking.disableNetworking import com.github.quiltservertools.ledger.utility.inspectBlock @@ -88,7 +89,7 @@ private fun onBlockPlace( context: ItemPlacementContext?, blockEntity: BlockEntity? ) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.blockPlaceAction( world, pos, @@ -106,7 +107,7 @@ private fun onBlockBreak( state: BlockState, blockEntity: BlockEntity? ) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.blockBreakAction( world, pos, @@ -121,12 +122,12 @@ private fun onItemPickUp( entity: ItemEntity, player: PlayerEntity ) { - DatabaseManager.logAction(ActionFactory.itemPickUpAction(entity, player)) + ActionQueueService.addToQueue(ActionFactory.itemPickUpAction(entity, player)) } private fun onItemDrop( entity: ItemEntity, player: PlayerEntity ) { - DatabaseManager.logAction(ActionFactory.itemDropAction(entity, player)) + ActionQueueService.addToQueue(ActionFactory.itemDropAction(entity, player)) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/WorldEventListener.kt b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/WorldEventListener.kt index 6cfdb1b7..1c1634d9 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/listeners/WorldEventListener.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/listeners/WorldEventListener.kt @@ -4,6 +4,7 @@ import com.github.quiltservertools.ledger.Ledger import com.github.quiltservertools.ledger.actionutils.ActionFactory import com.github.quiltservertools.ledger.callbacks.ItemInsertCallback import com.github.quiltservertools.ledger.callbacks.ItemRemoveCallback +import com.github.quiltservertools.ledger.database.ActionQueueService import com.github.quiltservertools.ledger.database.DatabaseManager import kotlinx.coroutines.launch import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents @@ -33,11 +34,11 @@ private fun onItemRemove( player: ServerPlayerEntity? ) { if (player != null) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.itemRemoveAction(world, stack, pos, player) ) } else { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.itemRemoveAction(world, stack, pos, source) ) } @@ -51,11 +52,11 @@ private fun onItemInsert( player: ServerPlayerEntity? ) { if (player != null) { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.itemInsertAction(world, stack, pos, player) ) } else { - DatabaseManager.logAction( + ActionQueueService.addToQueue( ActionFactory.itemInsertAction(world, stack, pos, source) ) } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/utility/Extensions.kt b/src/main/kotlin/com/github/quiltservertools/ledger/utility/Extensions.kt index f8694efc..3fcbea6c 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/utility/Extensions.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/utility/Extensions.kt @@ -8,6 +8,8 @@ import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.text.MutableText import net.minecraft.text.Text import net.minecraft.util.Identifier +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.times fun MutableText.appendWithSpace(text: Text) { this.append(text) @@ -22,3 +24,6 @@ fun ServerCommandSource.hasPlayer() = this.entity is ServerPlayerEntity // fun String.translate(vararg args: Any) = TranslatableText(this, args) fun MinecraftServer.getWorld(identifier: Identifier?) = getWorld(RegistryKey.of(RegistryKeys.WORLD, identifier)) + +val TICK_LENGTH = 50.milliseconds +inline val Int.ticks get() = this * TICK_LENGTH diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/utility/MessageUtils.kt b/src/main/kotlin/com/github/quiltservertools/ledger/utility/MessageUtils.kt index 1ff4efef..81823329 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/utility/MessageUtils.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/utility/MessageUtils.kt @@ -100,16 +100,16 @@ object MessageUtils { } fun warnBusy(source: ServerCommandSource) { - if (DatabaseManager.dbMutex.isLocked) { - source.sendFeedback( - { - Text.translatable( - "text.ledger.database.busy" - ).setStyle(TextColorPallet.primary) - }, - false - ) - } +// if (DatabaseManager.dbMutex.isLocked) { //TODO +// source.sendFeedback( +// { +// Text.translatable( +// "text.ledger.database.busy" +// ).setStyle(TextColorPallet.primary) +// }, +// false +// ) +// } } fun instantToText(time: Instant): MutableText { diff --git a/src/main/resources/ledger.toml b/src/main/resources/ledger.toml index b0651d9a..b04e48a8 100644 --- a/src/main/resources/ledger.toml +++ b/src/main/resources/ledger.toml @@ -5,6 +5,10 @@ queueTimeoutMin = 5 queueCheckDelaySec = 10 # Automatically purge entries older than the number of days specified. Set to -1 to disable autoPurgeDays = -1 +# The maximum amount of actions to commit in a single batch +batchSize = 1000 +# The amount of time to wait between batches in ticks (20 ticks = 1 second) +batchDelay = 10 [search] # Number of actions to show per page