Update world entities storage format to be more compact

This commit is contained in:
DBotThePony 2024-08-11 00:15:36 +07:00
parent ebd2dd3ab1
commit 4bd85c0e0e
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 112 additions and 87 deletions

View File

@ -51,6 +51,10 @@ object VersionRegistry {
return migrate(read, name) return migrate(read, name)
} }
fun load(reader: JsonElement): JsonElement {
return migrate(VersionedJson.fromJson(reader))
}
private val adapter by lazy { Starbound.gson.getAdapter(VersionedJson::class.java) } private val adapter by lazy { Starbound.gson.getAdapter(VersionedJson::class.java) }
fun load(): Future<*> { fun load(): Future<*> {

View File

@ -0,0 +1,44 @@
package ru.dbotthepony.kstarbound.io
import java.io.Closeable
import java.sql.Connection
import java.sql.PreparedStatement
class SQLSavepoint(connection: Connection, name: String) : Closeable {
private val begin: PreparedStatement
private val finish: PreparedStatement
private val rollback: PreparedStatement
init {
check('"' !in name) { "Invalid identifier: $name" }
begin = connection.prepareStatement("""
SAVEPOINT "$name"
""".trimIndent())
finish = connection.prepareStatement("""
RELEASE SAVEPOINT "$name"
""".trimIndent())
rollback = connection. prepareStatement("""
ROLLBACK TO SAVEPOINT "$name"
""".trimIndent())
}
fun execute(block: () -> Unit) {
try {
begin.execute()
block.invoke()
finish.execute()
} catch (err: Throwable) {
rollback.execute()
throw err
}
}
override fun close() {
begin.close()
finish.close()
rollback.close()
}
}

View File

@ -1,24 +0,0 @@
package ru.dbotthepony.kstarbound.io
import java.sql.Connection
import java.sql.PreparedStatement
data class SavepointStatements(val begin: PreparedStatement, val commit: PreparedStatement, val rollback: PreparedStatement)
fun Connection.createSavepoint(name: String): SavepointStatements {
check('"' !in name) { "Invalid identifier: $name" }
val begin = prepareStatement("""
SAVEPOINT "$name"
""".trimIndent())
val commit = prepareStatement("""
RELEASE SAVEPOINT "$name"
""".trimIndent())
val rollback = prepareStatement("""
ROLLBACK TO SAVEPOINT "$name"
""".trimIndent())
return SavepointStatements(begin, commit, rollback)
}

View File

@ -40,5 +40,9 @@ data class VersionedJson(val id: String, val version: Int?, val content: JsonEle
companion object { companion object {
private val adapter by lazy { Starbound.gson.getAdapter<VersionedJson>() } private val adapter by lazy { Starbound.gson.getAdapter<VersionedJson>() }
fun fromJson(data: JsonElement): VersionedJson {
return adapter.fromJsonTree(data)
}
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
@ -15,12 +16,13 @@ import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.VersionRegistry import ru.dbotthepony.kstarbound.VersionRegistry
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.io.createSavepoint import ru.dbotthepony.kstarbound.io.SQLSavepoint
import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.json.readJsonArrayInflated
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObjectInflated import ru.dbotthepony.kstarbound.json.readJsonObjectInflated
import ru.dbotthepony.kstarbound.json.writeJsonArrayDeflated
import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonObjectDeflated
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.CarriedExecutor
@ -39,7 +41,6 @@ import java.io.File
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.sql.Connection import java.sql.Connection
import java.sql.DriverManager import java.sql.DriverManager
import java.sql.PreparedStatement
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.Deflater import java.util.zip.Deflater
@ -96,28 +97,26 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() {
it.execute(""" it.execute("""
CREATE TABLE IF NOT EXISTS "entities" ( CREATE TABLE IF NOT EXISTS "entities" (
"x" INTEGER NOT NULL,
"y" INTEGER NOT NULL,
"data" BLOB NOT NULL,
PRIMARY KEY ("x", "y")
)
""".trimIndent())
it.execute("""
CREATE TABLE IF NOT EXISTS "unique_entities" (
"unique_id" BLOB NOT NULL PRIMARY KEY,
-- store chunks because rules for entities belonging to specific chunk might get different over time -- store chunks because rules for entities belonging to specific chunk might get different over time
"chunkX" INTEGER NOT NULL, "chunkX" INTEGER NOT NULL,
"chunkY" INTEGER NOT NULL, "chunkY" INTEGER NOT NULL,
"x" REAL NOT NULL, "x" REAL NOT NULL,
"y" REAL NOT NULL, "y" REAL NOT NULL
"unique_id" VARCHAR,
"type" VARCHAR NOT NULL,
"version" INTEGER NOT NULL,
"data" BLOB NOT NULL
) )
""".trimIndent()) """.trimIndent())
// Enforce unique-ness of unique entity IDs
// If another entity pops-up with same unique id, then it will overwrite entity in other chunk
// (unless colliding entities are both loaded into world's memory, which will cause runtime exception to be thrown;
// and no overwrite will happen)
it.execute(""" it.execute("""
CREATE UNIQUE INDEX IF NOT EXISTS "entities_unique_id" ON "entities" ("unique_id") CREATE INDEX IF NOT EXISTS "entities_chunk_pos" ON "unique_entities" ("chunkX", "chunkY")
""".trimIndent())
it.execute("""
CREATE INDEX IF NOT EXISTS "entities_chunk_pos" ON "entities" ("chunkX", "chunkY")
""".trimIndent()) """.trimIndent())
} }
@ -167,7 +166,7 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() {
} }
private val readEntities = connection.prepareStatement(""" private val readEntities = connection.prepareStatement("""
SELECT "type", "version", "data" FROM "entities" WHERE "chunkX" = ? AND "chunkY" = ? SELECT "data" FROM "entities" WHERE "x" = ? AND "y" = ?
""".trimIndent()) """.trimIndent())
override fun loadEntities(pos: ChunkPos): CompletableFuture<Collection<AbstractEntity>> { override fun loadEntities(pos: ChunkPos): CompletableFuture<Collection<AbstractEntity>> {
@ -178,20 +177,22 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() {
val entities = ArrayList<AbstractEntity>() val entities = ArrayList<AbstractEntity>()
readEntities.executeQuery().use { readEntities.executeQuery().use {
while (it.next()) { if (it.next()) {
val rtype = it.getString(1) val data = it.getBytes(1).readJsonArrayInflated()
val version = it.getInt(2)
val type = EntityType.entries.firstOrNull { it.storeName == rtype }
if (type != null) { for (entry in data) {
try { val versioned = VersionedJson.fromJson(entry)
val data = it.getBytes(3).readJsonObjectInflated() val type = EntityType.entries.firstOrNull { it.storeName == versioned.id }
entities.add(type.fromStorage(VersionRegistry.migrate(VersionedJson(rtype, version, data)) as JsonObject))
} catch (err: Throwable) { if (type != null) {
LOGGER.error("Unable to deserialize entity in chunk $pos", err) try {
entities.add(type.fromStorage(VersionRegistry.migrate(versioned) as JsonObject))
} catch (err: Throwable) {
LOGGER.error("Unable to deserialize entity in chunk $pos", err)
}
} else {
LOGGER.error("Unknown entity type ${versioned.id} in chunk $pos")
} }
} else {
LOGGER.error("Unknown entity type $rtype in chunk $pos")
} }
} }
} }
@ -240,57 +241,53 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() {
} }
} }
private val clearEntities = connection.prepareStatement(""" private val clearUniqueEntities = connection.prepareStatement("""
DELETE FROM "entities" WHERE "chunkX" = ? AND "chunkY" = ? DELETE FROM "unique_entities" WHERE "chunkX" = ? AND "chunkY" = ?
""".trimIndent()) """.trimIndent())
private val beginSaveEntities: PreparedStatement private val entitiesSavepoint = SQLSavepoint(connection, "save_entities")
private val finishSaveEntities: PreparedStatement
private val rollbackSaveEntities: PreparedStatement
init { private val writeEntities = connection.prepareStatement("""
val (begin, commit, rollback) = connection.createSavepoint("save_entities") REPLACE INTO "entities" ("x", "y", "data")
beginSaveEntities = begin VALUES (?, ?, ?)
finishSaveEntities = commit """.trimIndent())
rollbackSaveEntities = rollback
}
private val writeEntity = connection.prepareStatement(""" private val writeUniqueEntity = connection.prepareStatement("""
REPLACE INTO "entities" ("chunkX", "chunkY", "x", "y", "unique_id", "type", "version", "data") REPLACE INTO "unique_entities" ("unique_id", "chunkX", "chunkY", "x", "y")
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""".trimIndent()) """.trimIndent())
override fun saveEntities(pos: ChunkPos, entities: Collection<AbstractEntity>) { override fun saveEntities(pos: ChunkPos, entities: Collection<AbstractEntity>) {
executor.execute { executor.execute {
beginSaveEntities.execute() entitiesSavepoint.execute {
clearUniqueEntities.setInt(1, pos.x)
clearUniqueEntities.setInt(2, pos.y)
clearUniqueEntities.execute()
try { val storeData = JsonArray()
clearEntities.setInt(1, pos.x)
clearEntities.setInt(2, pos.y)
clearEntities.execute()
for (entity in entities) { for (entity in entities) {
Starbound.storeJson { Starbound.storeJson {
val data = JsonObject() val data = JsonObject()
entity.serialize(data) entity.serialize(data)
storeData.add(VersionRegistry.make(entity.type.storeName, data).toJson())
writeEntity.setInt(1, pos.x) if (entity.uniqueID.get() != null) {
writeEntity.setInt(2, pos.y) writeUniqueEntity.setString(1, entity.uniqueID.get())
writeEntity.setDouble(3, entity.position.x) writeUniqueEntity.setInt(2, pos.x)
writeEntity.setDouble(4, entity.position.y) writeUniqueEntity.setInt(3, pos.y)
writeEntity.setString(5, entity.uniqueID.get()) writeUniqueEntity.setDouble(4, entity.position.x)
writeEntity.setString(6, entity.type.storeName) writeUniqueEntity.setDouble(5, entity.position.y)
writeEntity.setInt(7, VersionRegistry.currentVersion(entity.type.storeName))
writeEntity.setBytes(8, data.writeJsonObjectDeflated())
writeEntity.execute() writeUniqueEntity.execute()
}
} }
} }
finishSaveEntities.execute() writeEntities.setInt(1, pos.x)
} catch (err: Throwable) { writeEntities.setInt(2, pos.y)
rollbackSaveEntities.execute() writeEntities.setBytes(3, storeData.writeJsonArrayDeflated())
throw err writeEntities.execute()
} }
} }
} }
@ -430,7 +427,7 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() {
} }
private val findUniqueEntity = connection.prepareStatement(""" private val findUniqueEntity = connection.prepareStatement("""
SELECT "chunkX", "chunkY", "x", "y" FROM "entities" WHERE "unique_id" = ? SELECT "chunkX", "chunkY", "x", "y" FROM "unique_entities" WHERE "unique_id" = ?
""".trimIndent()) """.trimIndent())
override fun findUniqueEntity(identifier: String): CompletableFuture<UniqueEntitySearchResult?> { override fun findUniqueEntity(identifier: String): CompletableFuture<UniqueEntitySearchResult?> {