From f1b71c2ac9bbad6ccfe23522d172444e1e2382ac Mon Sep 17 00:00:00 2001 From: Auxilor Date: Wed, 21 Aug 2024 18:07:09 +0100 Subject: [PATCH 01/10] Started work on new persistent data --- .../data/handlers/PersistentDataHandler.java | 67 +++++++++++ .../data/handlers/PersistentDataHandlers.java | 42 +++++++ .../core/data/handlers/SerializedProfile.java | 20 ++++ .../handlers/MongoPersistentDataHandler.kt | 108 ++++++++++++++++++ .../handlers/YamlPersistentDataHandler.kt | 63 ++++++++++ .../spigot/data/storage/MongoDataHandler.kt | 7 +- 6 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java create mode 100644 eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java create mode 100644 eco-api/src/main/java/com/willfp/eco/core/data/handlers/SerializedProfile.java create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java new file mode 100644 index 000000000..1f782f058 --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java @@ -0,0 +1,67 @@ +package com.willfp.eco.core.data.handlers; + +import com.willfp.eco.core.data.keys.PersistentDataKey; +import com.willfp.eco.core.registry.Registrable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.UUID; + +public abstract class PersistentDataHandler implements Registrable { + /** + * The id of the handler. + */ + private final String id; + + /** + * Create a new persistent data handler. + * + * @param id The id of the handler. + */ + protected PersistentDataHandler(@NotNull final String id) { + this.id = id; + } + + @Override + public @NotNull String getID() { + return id; + } + + /** + * Read a key from persistent data. + * + * @param uuid The uuid. + * @param key The key. + * @param The type of the key. + * @return The value, or null if not found. + */ + @Nullable + public abstract T read(@NotNull UUID uuid, @NotNull PersistentDataKey key); + + /** + * Write a key to persistent data. + * + * @param uuid The uuid. + * @param key The key. + * @param value The value. + * @param The type of the key. + */ + public abstract void write(@NotNull UUID uuid, @NotNull PersistentDataKey key, @NotNull T value); + + /** + * Serialize data. + * + * @param keys The keys to serialize. + * @return The serialized data. + */ + @NotNull + public abstract Set serializeData(@NotNull final Set> keys); + + /** + * Load profile data. + * + * @param data The data. + */ + public abstract void loadProfileData(@NotNull Set data); +} diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java new file mode 100644 index 000000000..6f447ac23 --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java @@ -0,0 +1,42 @@ +package com.willfp.eco.core.data.handlers; + +import com.willfp.eco.core.registry.Registry; +import org.jetbrains.annotations.NotNull; + +/** + * Utility class to manage persistent data handlers. + */ +public final class PersistentDataHandlers { + private static final Registry REGISTRY = new Registry<>(); + + /** + * Register a persistent data handler. + * + * @param handler The handler. + */ + public static void register(@NotNull final PersistentDataHandler handler) { + REGISTRY.register(handler); + } + + /** + * Get a persistent data handler by id. + * + * @param id The id. + * @return The handler. + * @throws IllegalArgumentException if no handler with that id is found. + */ + @NotNull + public static PersistentDataHandler get(@NotNull final String id) { + PersistentDataHandler handler = REGISTRY.get(id); + + if (handler == null) { + throw new IllegalArgumentException("No handler with id: " + id); + } + + return handler; + } + + private PersistentDataHandlers() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } +} diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/SerializedProfile.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/SerializedProfile.java new file mode 100644 index 000000000..e8093c5d0 --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/SerializedProfile.java @@ -0,0 +1,20 @@ +package com.willfp.eco.core.data.handlers; + +import com.willfp.eco.core.data.keys.PersistentDataKey; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; + +/** + * Serialized profile. + * + * @param uuid The uuid. + * @param data The data. + */ +public record SerializedProfile( + @NotNull UUID uuid, + @NotNull Map, Object> data +) { + +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt new file mode 100644 index 000000000..58d33d9e0 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt @@ -0,0 +1,108 @@ +package com.willfp.eco.internal.spigot.data.handlers + +import com.willfp.eco.core.config.interfaces.Config +import com.willfp.eco.core.data.handlers.PersistentDataHandler +import com.willfp.eco.core.data.handlers.SerializedProfile +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.core.data.keys.PersistentDataKeyType +import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import java.util.UUID +import java.util.concurrent.Executors +import com.mongodb.kotlin.client.coroutine.MongoClient +import com.willfp.eco.core.config.Configs +import com.willfp.eco.internal.spigot.data.storage.UUIDProfile +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.forEach +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.bson.Document + +class MongoPersistentDataHandler( + config: Config, + plugin: EcoSpigotPlugin +) : PersistentDataHandler("yaml") { + + private val url: String = config.getString("url") ?: error("MongoDB URL not found in config") + private val databaseName: String = config.getString("database") ?: error("Database name not found in config") + private val client = MongoClient.create(url) + private val database = client.getDatabase(databaseName) + private val collection = database.getCollection("uuidprofile") + private val executor = Executors.newCachedThreadPool() + + override fun read(uuid: UUID, key: PersistentDataKey): T? { + return runBlocking { + doRead(uuid, key) + } + } + + private suspend fun doRead(uuid: UUID, key: PersistentDataKey): T? { + val document = collection.find(Document("uuid", uuid.toString())).firstOrNull() ?: return null + val data = document.data[key.key.toString()] as? T + + return data + } + + override fun write(uuid: UUID, key: PersistentDataKey, value: T) { + executor.submit { + runBlocking { + doWrite(uuid, key, value) + } + } + } + + private suspend fun doWrite(uuid: UUID, key: PersistentDataKey, value: T) { + val document = collection.find(Document("uuid", uuid.toString())).firstOrNull() ?: return null + document.data[key.key.toString()] = value + + collection.replaceOne(Document("uuid", uuid.toString()), document) + } + + override fun serializeData(keys: Set>): Set { + val profiles = mutableSetOf() + + collection.find().forEach { document -> + val uuid = UUID.fromString(document.getString("uuid")) + val data = document.get("data") as Document + val profileData = keys.associateWith { key -> + when (key.type) { + PersistentDataKeyType.STRING -> data.getString(key.key.key) + PersistentDataKeyType.BOOLEAN -> data.getBoolean(key.key.key) + PersistentDataKeyType.INT -> data.getInteger(key.key.key) + PersistentDataKeyType.DOUBLE -> data.getDouble(key.key.key) + PersistentDataKeyType.STRING_LIST -> data.getList(key.key.key, String::class.java) + PersistentDataKeyType.BIG_DECIMAL -> data.getDecimal128(key.key.key)?.bigDecimalValue() + PersistentDataKeyType.CONFIG -> data.get(key.key.key) + else -> null + } ?: key.defaultValue + } + + profiles.add(SerializedProfile(uuid, profileData as Map, Any>)) + } + + return profiles + } + + override fun loadProfileData(data: Set) { + data.forEach { profile -> + val document = Document("uuid", profile.uuid.toString()) + val profileData = Document() + + profile.data.forEach { (key, value) -> + profileData.put(key.key.key, value) + } + + document.put("data", profileData) + collection.replaceOne(Document("uuid", profile.uuid.toString()), document, com.mongodb.client.model.ReplaceOptions().upsert(true)) + } + } + + @Serializable + internal data class UUIDProfile( + // Storing UUID as strings for serialization + @SerialName("_id") val uuid: String, + // Storing NamespacedKeys as strings for serialization + val data: MutableMap + ) +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt new file mode 100644 index 000000000..7b10d4bc1 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt @@ -0,0 +1,63 @@ +package com.willfp.eco.internal.spigot.data.handlers + +import com.willfp.eco.core.data.handlers.PersistentDataHandler +import com.willfp.eco.core.data.handlers.SerializedProfile +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.core.data.keys.PersistentDataKeyType +import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import java.util.UUID + +class YamlPersistentDataHandler( + plugin: EcoSpigotPlugin +) : PersistentDataHandler("yaml") { + private val dataYml = plugin.dataYml + + @Suppress("UNCHECKED_CAST") + override fun read(uuid: UUID, key: PersistentDataKey): T? { + // Separate `as T?` for each branch to prevent compiler warnings. + val value = when (key.type) { + PersistentDataKeyType.INT -> dataYml.getIntOrNull("player.$uuid.${key.key}") as T? + PersistentDataKeyType.DOUBLE -> dataYml.getDoubleOrNull("player.$uuid.${key.key}") as T? + PersistentDataKeyType.STRING -> dataYml.getStringOrNull("player.$uuid.${key.key}") as T? + PersistentDataKeyType.BOOLEAN -> dataYml.getBoolOrNull("player.$uuid.${key.key}") as T? + PersistentDataKeyType.STRING_LIST -> dataYml.getStringsOrNull("player.$uuid.${key.key}") as T? + PersistentDataKeyType.CONFIG -> dataYml.getSubsectionOrNull("player.$uuid.${key.key}") as T? + PersistentDataKeyType.BIG_DECIMAL -> dataYml.getBigDecimalOrNull("player.$uuid.${key.key}") as T? + + else -> null + } + + return value + } + + override fun write(uuid: UUID, key: PersistentDataKey, value: T) { + dataYml.set("player.$uuid.$key", value) + } + + override fun serializeData(keys: Set>): Set { + val profiles = mutableSetOf() + val uuids = dataYml.getSubsection("player").getKeys(false).map { UUID.fromString(it) } + + for (uuid in uuids) { + val data = mutableMapOf, Any>() + + for (key in keys) { + data[key] = read(uuid, key) ?: continue + } + + profiles.add(SerializedProfile(uuid, data)) + } + + return profiles + } + + override fun loadProfileData(data: Set) { + for (profile in data) { + for ((key, value) in profile.data) { + // Dirty cast, but it's fine because we know it's the same type + @Suppress("UNCHECKED_CAST") + write(profile.uuid, key as PersistentDataKey, value as Any) + } + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt index 67c12768b..a00c811bb 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt @@ -2,7 +2,6 @@ package com.willfp.eco.internal.spigot.data.storage import com.mongodb.client.model.Filters import com.mongodb.client.model.ReplaceOptions -import com.mongodb.client.model.UpdateOptions import com.mongodb.client.model.Updates import com.mongodb.kotlin.client.coroutine.MongoClient import com.mongodb.kotlin.client.coroutine.MongoCollection @@ -11,15 +10,13 @@ import com.willfp.eco.internal.spigot.EcoSpigotPlugin import com.willfp.eco.internal.spigot.data.ProfileHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.bson.codecs.pojo.annotations.BsonId -import java.util.UUID -import kotlinx.coroutines.flow.firstOrNull import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import org.bukkit.Bukkit +import java.util.UUID @Suppress("UNCHECKED_CAST") class MongoDataHandler( From fd031e21f5c758f31d92d206c0b2ade4d4368d56 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sat, 24 Aug 2024 16:14:15 +0100 Subject: [PATCH 02/10] Added new data handlers --- .../data/handlers/DataTypeSerializer.java | 43 ++++ .../data/handlers/PersistentDataHandler.java | 134 +++++++++- .../core/data/keys/PersistentDataKeyType.java | 48 +++- .../LegacyMySQLPersistentDataHandler.kt | 113 +++++++++ .../handlers/MongoPersistentDataHandler.kt | 135 +++++----- .../handlers/MySQLPersistentDataHandler.kt | 235 ++++++++++++++++++ .../handlers/YamlPersistentDataHandler.kt | 82 +++--- .../core-plugin/src/main/resources/config.yml | 9 +- 8 files changed, 682 insertions(+), 117 deletions(-) create mode 100644 eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java new file mode 100644 index 000000000..840f5e2ec --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java @@ -0,0 +1,43 @@ +package com.willfp.eco.core.data.handlers; + +import com.willfp.eco.core.data.keys.PersistentDataKey; +import com.willfp.eco.core.data.keys.PersistentDataKeyType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Handles data read/write for a {@link com.willfp.eco.core.data.keys.PersistentDataKeyType} for a specific + * data handler. + */ +public abstract class DataTypeSerializer { + /** + * Create a new data type serializer. + */ + protected DataTypeSerializer() { + + } + + /** + * Read a value. + * + * @param uuid The uuid. + * @param key The key. + * @return The value. + */ + @Nullable + public abstract T readAsync(@NotNull final UUID uuid, + @NotNull final PersistentDataKey key); + + /** + * Write a value. + * + * @param uuid The uuid. + * @param key The key. + * @param value The value. + */ + public abstract void writeAsync(@NotNull final UUID uuid, + @NotNull final PersistentDataKey key, + @NotNull final T value); +} diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java index 1f782f058..a74d156b0 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java @@ -5,8 +5,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; public abstract class PersistentDataHandler implements Registrable { /** @@ -14,6 +22,11 @@ public abstract class PersistentDataHandler implements Registrable { */ private final String id; + /** + * The executor. + */ + private final ExecutorService executor = Executors.newCachedThreadPool(); + /** * Create a new persistent data handler. * @@ -23,9 +36,38 @@ protected PersistentDataHandler(@NotNull final String id) { this.id = id; } - @Override - public @NotNull String getID() { - return id; + /** + * Get all UUIDs with saved data. + * + * @return All saved UUIDs. + */ + protected abstract Set getSavedUUIDs(); + + /** + * Save to disk. + *

+ * If write commits to disk, this method does not need to be overridden. + *

+ * This method is called asynchronously. + */ + protected void doSave() { + // Save to disk + } + + /** + * If the handler should autosave. + * + * @return If the handler should autosave. + */ + public boolean shouldAutosave() { + return true; + } + + /** + * Save the data. + */ + public final void save() { + executor.submit(this::doSave); } /** @@ -37,7 +79,18 @@ protected PersistentDataHandler(@NotNull final String id) { * @return The value, or null if not found. */ @Nullable - public abstract T read(@NotNull UUID uuid, @NotNull PersistentDataKey key); + public final T read(@NotNull final UUID uuid, + @NotNull final PersistentDataKey key) { + DataTypeSerializer serializer = key.getType().getSerializer(this); + Future future = executor.submit(() -> serializer.readAsync(uuid, key)); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + return null; + } + } /** * Write a key to persistent data. @@ -47,7 +100,12 @@ protected PersistentDataHandler(@NotNull final String id) { * @param value The value. * @param The type of the key. */ - public abstract void write(@NotNull UUID uuid, @NotNull PersistentDataKey key, @NotNull T value); + public final void write(@NotNull final UUID uuid, + @NotNull final PersistentDataKey key, + @NotNull final T value) { + DataTypeSerializer serializer = key.getType().getSerializer(this); + executor.submit(() -> serializer.writeAsync(uuid, key, value)); + } /** * Serialize data. @@ -56,12 +114,74 @@ protected PersistentDataHandler(@NotNull final String id) { * @return The serialized data. */ @NotNull - public abstract Set serializeData(@NotNull final Set> keys); + public final Set serializeData(@NotNull final Set> keys) { + Set profiles = new HashSet<>(); + + for (UUID uuid : getSavedUUIDs()) { + Map, Object> data = new HashMap<>(); + + for (PersistentDataKey key : keys) { + Object value = read(uuid, key); + data.put(key, value); + } + + profiles.add(new SerializedProfile(uuid, data)); + } + + return profiles; + } /** * Load profile data. * * @param data The data. */ - public abstract void loadProfileData(@NotNull Set data); + @SuppressWarnings("unchecked") + public final void loadProfileData(@NotNull Set data) { + for (SerializedProfile profile : data) { + for (Map.Entry, Object> entry : profile.data().entrySet()) { + PersistentDataKey key = entry.getKey(); + Object value = entry.getValue(); + + // This cast is safe because the data is serialized + write(profile.uuid(), (PersistentDataKey) key, value); + } + } + } + + /** + * Await outstanding writes. + */ + public final void awaitOutstandingWrites() throws InterruptedException { + boolean success = executor.awaitTermination(15, TimeUnit.SECONDS); + + if (!success) { + throw new InterruptedException("Failed to await outstanding writes"); + } + } + + @Override + public final @NotNull String getID() { + return id; + } + + @Override + public final boolean equals(@Nullable final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + PersistentDataHandler that = (PersistentDataHandler) obj; + + return id.equals(that.id); + } + + @Override + public final int hashCode() { + return id.hashCode(); + } } diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKeyType.java b/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKeyType.java index 97bb24227..d8cd4c4e9 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKeyType.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKeyType.java @@ -1,12 +1,16 @@ package com.willfp.eco.core.data.keys; import com.willfp.eco.core.config.interfaces.Config; +import com.willfp.eco.core.data.handlers.DataTypeSerializer; +import com.willfp.eco.core.data.handlers.PersistentDataHandler; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -60,24 +64,58 @@ public final class PersistentDataKeyType { */ private final String name; + /** + * The serializers for this key type. + */ + private final Map> serializers = new HashMap<>(); + + /** + * Create new PersistentDataKeyType. + * + * @param name The name. + */ + private PersistentDataKeyType(@NotNull final String name) { + VALUES.add(this); + + this.name = name; + } + /** * Get the name of the key type. * * @return The name. */ + @NotNull public String name() { return name; } /** - * Create new PersistentDataKeyType. + * Register a serializer for this key type. * - * @param name The name. + * @param handler The handler. + * @param serializer The serializer. */ - private PersistentDataKeyType(@NotNull final String name) { - VALUES.add(this); + public void registerSerializer(@NotNull final PersistentDataHandler handler, + @NotNull final DataTypeSerializer serializer) { + this.serializers.put(handler, serializer); + } - this.name = name; + /** + * Get the serializer for a handler. + * + * @param handler The handler. + * @return The serializer. + */ + @NotNull + public DataTypeSerializer getSerializer(@NotNull final PersistentDataHandler handler) { + DataTypeSerializer serializer = this.serializers.get(handler); + + if (serializer == null) { + throw new IllegalArgumentException("No serializer for handler: " + handler); + } + + return serializer; } @Override diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt new file mode 100644 index 000000000..0672c8b1b --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt @@ -0,0 +1,113 @@ +package com.willfp.eco.internal.spigot.data.handlers + +import com.willfp.eco.core.config.ConfigType +import com.willfp.eco.core.config.Configs +import com.willfp.eco.core.config.interfaces.Config +import com.willfp.eco.core.config.readConfig +import com.willfp.eco.core.data.handlers.DataTypeSerializer +import com.willfp.eco.core.data.handlers.PersistentDataHandler +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.core.data.keys.PersistentDataKeyType +import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import eu.decentsoftware.holograms.api.utils.scheduler.S +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.Table.Dual.decimal +import org.jetbrains.exposed.sql.Table.Dual.double +import org.jetbrains.exposed.sql.Table.Dual.varchar +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import java.math.BigDecimal +import java.util.UUID + +class LegacyMySQLPersistentDataHandler( + plugin: EcoSpigotPlugin, + config: Config +) : PersistentDataHandler("mysql_legacy") { + private val dataSource = HikariDataSource(HikariConfig().apply { + driverClassName = "com.mysql.cj.jdbc.Driver" + username = config.getString("user") + password = config.getString("password") + jdbcUrl = "jdbc:mysql://" + + "${config.getString("host")}:" + + "${config.getString("port")}/" + + config.getString("database") + maximumPoolSize = config.getInt("connections") + }) + + private val database = Database.connect(dataSource) + + private val table = object : UUIDTable("eco_data") { + val data = text("json_data") + } + + init { + transaction(database) { + SchemaUtils.create(table) + } + + PersistentDataKeyType.STRING.registerSerializer(this, LegacySerializer()) + PersistentDataKeyType.BOOLEAN.registerSerializer(this, LegacySerializer()) + PersistentDataKeyType.INT.registerSerializer(this, LegacySerializer()) + PersistentDataKeyType.DOUBLE.registerSerializer(this, LegacySerializer()) + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, LegacySerializer()) + PersistentDataKeyType.CONFIG.registerSerializer(this, LegacySerializer()) + PersistentDataKeyType.STRING_LIST.registerSerializer(this, LegacySerializer>()) + } + + override fun getSavedUUIDs(): Set { + return transaction(database) { + table.selectAll() + .map { it[table.id] } + .toSet() + }.map { it.value }.toSet() + } + + private inner class LegacySerializer : DataTypeSerializer() { + override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { + val json = transaction(database) { + table.select { table.id eq uuid } + .limit(1) + .singleOrNull() + ?.get(table.data) + } + + if (json == null) { + return null + } + + val data = readConfig(json, ConfigType.JSON) + + val value: Any? = when (key.type) { + PersistentDataKeyType.INT -> data.getIntOrNull(key.key.toString()) + PersistentDataKeyType.DOUBLE -> data.getDoubleOrNull(key.key.toString()) + PersistentDataKeyType.STRING -> data.getStringOrNull(key.key.toString()) + PersistentDataKeyType.BOOLEAN -> data.getBoolOrNull(key.key.toString()) + PersistentDataKeyType.STRING_LIST -> data.getStringsOrNull(key.key.toString()) + PersistentDataKeyType.CONFIG -> data.getSubsectionOrNull(key.key.toString()) + PersistentDataKeyType.BIG_DECIMAL -> data.getBigDecimalOrNull(key.key.toString()) + + else -> null + } + + @Suppress("UNCHECKED_CAST") + return value as? T? + } + + override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { + throw UnsupportedOperationException("Legacy MySQL does not support writing") + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt index 58d33d9e0..ba69d1e6a 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt @@ -1,107 +1,112 @@ package com.willfp.eco.internal.spigot.data.handlers +import com.mongodb.client.model.Filters +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.kotlin.client.coroutine.MongoClient +import com.willfp.eco.core.config.Configs import com.willfp.eco.core.config.interfaces.Config +import com.willfp.eco.core.data.handlers.DataTypeSerializer import com.willfp.eco.core.data.handlers.PersistentDataHandler -import com.willfp.eco.core.data.handlers.SerializedProfile import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType import com.willfp.eco.internal.spigot.EcoSpigotPlugin -import java.util.UUID -import java.util.concurrent.Executors -import com.mongodb.kotlin.client.coroutine.MongoClient -import com.willfp.eco.core.config.Configs -import com.willfp.eco.internal.spigot.data.storage.UUIDProfile import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.forEach +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import org.bson.Document +import java.math.BigDecimal +import java.util.UUID class MongoPersistentDataHandler( - config: Config, - plugin: EcoSpigotPlugin -) : PersistentDataHandler("yaml") { - - private val url: String = config.getString("url") ?: error("MongoDB URL not found in config") - private val databaseName: String = config.getString("database") ?: error("Database name not found in config") - private val client = MongoClient.create(url) - private val database = client.getDatabase(databaseName) + plugin: EcoSpigotPlugin, + config: Config +) : PersistentDataHandler("mongo") { + private val client = MongoClient.create(config.getString("url")) + private val database = client.getDatabase(config.getString("database")) + + // Collection name is set for backwards compatibility private val collection = database.getCollection("uuidprofile") - private val executor = Executors.newCachedThreadPool() - override fun read(uuid: UUID, key: PersistentDataKey): T? { - return runBlocking { - doRead(uuid, key) - } - } + init { + PersistentDataKeyType.STRING.registerSerializer(this, MongoSerializer()) + PersistentDataKeyType.BOOLEAN.registerSerializer(this, MongoSerializer()) + PersistentDataKeyType.INT.registerSerializer(this, MongoSerializer()) + PersistentDataKeyType.DOUBLE.registerSerializer(this, MongoSerializer()) + PersistentDataKeyType.STRING_LIST.registerSerializer(this, MongoSerializer>()) - private suspend fun doRead(uuid: UUID, key: PersistentDataKey): T? { - val document = collection.find(Document("uuid", uuid.toString())).firstOrNull() ?: return null - val data = document.data[key.key.toString()] as? T + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : MongoSerializer() { + override fun convertToMongo(value: BigDecimal): Any { + return value.toString() + } - return data - } + override fun convertFromMongo(value: Any): BigDecimal { + return BigDecimal(value.toString()) + } + }) - override fun write(uuid: UUID, key: PersistentDataKey, value: T) { - executor.submit { - runBlocking { - doWrite(uuid, key, value) + PersistentDataKeyType.CONFIG.registerSerializer(this, object : MongoSerializer() { + override fun convertToMongo(value: Config): Any { + return value.toMap() + } + + @Suppress("UNCHECKED_CAST") + override fun convertFromMongo(value: Any): Config { + return Configs.fromMap(value as Map) } + }) + } + + override fun getSavedUUIDs(): Set { + return runBlocking { + collection.find().toList().map { UUID.fromString(it.uuid) }.toSet() } } - private suspend fun doWrite(uuid: UUID, key: PersistentDataKey, value: T) { - val document = collection.find(Document("uuid", uuid.toString())).firstOrNull() ?: return null - document.data[key.key.toString()] = value + private open inner class MongoSerializer : DataTypeSerializer() { + override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { + return runBlocking { + val profile = collection.find(Filters.eq("uuid", uuid.toString())).firstOrNull() + ?: return@runBlocking null - collection.replaceOne(Document("uuid", uuid.toString()), document) - } + val value = profile.data[key.key.toString()] + ?: return@runBlocking null - override fun serializeData(keys: Set>): Set { - val profiles = mutableSetOf() - - collection.find().forEach { document -> - val uuid = UUID.fromString(document.getString("uuid")) - val data = document.get("data") as Document - val profileData = keys.associateWith { key -> - when (key.type) { - PersistentDataKeyType.STRING -> data.getString(key.key.key) - PersistentDataKeyType.BOOLEAN -> data.getBoolean(key.key.key) - PersistentDataKeyType.INT -> data.getInteger(key.key.key) - PersistentDataKeyType.DOUBLE -> data.getDouble(key.key.key) - PersistentDataKeyType.STRING_LIST -> data.getList(key.key.key, String::class.java) - PersistentDataKeyType.BIG_DECIMAL -> data.getDecimal128(key.key.key)?.bigDecimalValue() - PersistentDataKeyType.CONFIG -> data.get(key.key.key) - else -> null - } ?: key.defaultValue + convertFromMongo(value) } + } - profiles.add(SerializedProfile(uuid, profileData as Map, Any>)) + protected open fun convertToMongo(value: T): Any { + return value } - return profiles - } + override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { + runBlocking { + val profile = collection.find(Filters.eq("uuid", uuid.toString())).firstOrNull() + ?: UUIDProfile(uuid.toString(), mutableMapOf()) - override fun loadProfileData(data: Set) { - data.forEach { profile -> - val document = Document("uuid", profile.uuid.toString()) - val profileData = Document() + profile.data[key.key.toString()] = convertToMongo(value) - profile.data.forEach { (key, value) -> - profileData.put(key.key.key, value) + collection.replaceOne( + Filters.eq("uuid", uuid.toString()), + profile, + ReplaceOptions().upsert(true) + ) } + } - document.put("data", profileData) - collection.replaceOne(Document("uuid", profile.uuid.toString()), document, com.mongodb.client.model.ReplaceOptions().upsert(true)) + protected open fun convertFromMongo(value: Any): T { + @Suppress("UNCHECKED_CAST") + return value as T } } @Serializable - internal data class UUIDProfile( + private data class UUIDProfile( // Storing UUID as strings for serialization @SerialName("_id") val uuid: String, + // Storing NamespacedKeys as strings for serialization val data: MutableMap ) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt new file mode 100644 index 000000000..10f1edd37 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt @@ -0,0 +1,235 @@ +package com.willfp.eco.internal.spigot.data.handlers + +import com.willfp.eco.core.config.ConfigType +import com.willfp.eco.core.config.Configs +import com.willfp.eco.core.config.interfaces.Config +import com.willfp.eco.core.config.readConfig +import com.willfp.eco.core.data.handlers.DataTypeSerializer +import com.willfp.eco.core.data.handlers.PersistentDataHandler +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.core.data.keys.PersistentDataKeyType +import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import eu.decentsoftware.holograms.api.utils.scheduler.S +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.TextColumnType +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import java.math.BigDecimal +import java.util.UUID + +class MySQLPersistentDataHandler( + plugin: EcoSpigotPlugin, + config: Config +) : PersistentDataHandler("mysql") { + private val dataSource = HikariDataSource(HikariConfig().apply { + driverClassName = "com.mysql.cj.jdbc.Driver" + username = config.getString("user") + password = config.getString("password") + jdbcUrl = "jdbc:mysql://" + + "${config.getString("host")}:" + + "${config.getString("port")}/" + + config.getString("database") + maximumPoolSize = config.getInt("connections") + }) + + private val prefix = config.getString("prefix") + + private val database = Database.connect(dataSource) + + init { + PersistentDataKeyType.STRING.registerSerializer(this, object : DirectStoreSerializer() { + override val table = object : KeyTable("string") { + override val value = varchar("value", 128) + } + }) + + PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : DirectStoreSerializer() { + override val table = object : KeyTable("boolean") { + override val value = bool("value") + } + }) + + PersistentDataKeyType.INT.registerSerializer(this, object : DirectStoreSerializer() { + override val table = object : KeyTable("int") { + override val value = integer("value") + } + }) + + PersistentDataKeyType.DOUBLE.registerSerializer(this, object : DirectStoreSerializer() { + override val table = object : KeyTable("double") { + override val value = double("value") + } + }) + + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : DirectStoreSerializer() { + override val table = object : KeyTable("big_decimal") { + // 34 digits of precision, 4 digits of scale + override val value = decimal("value", 34, 4) + } + }) + + PersistentDataKeyType.CONFIG.registerSerializer(this, object : SingleValueSerializer() { + override val table = object : KeyTable("config") { + override val value = text("value") + } + + override fun convertFromStored(value: String): Config { + return readConfig(value, ConfigType.JSON) + } + + override fun convertToStored(value: Config): String { + // Store config as JSON + return if (value.type == ConfigType.JSON) { + value.toPlaintext() + } else { + Configs.fromMap(value.toMap(), ConfigType.JSON).toPlaintext() + } + } + }) + + PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MultiValueSerializer() { + override val table = object : ListKeyTable("string_list") { + override val value = varchar("value", 128) + } + }) + } + + override fun getSavedUUIDs(): Set { + val savedUUIDs = mutableSetOf() + + for (keyType in PersistentDataKeyType.values()) { + val serializer = keyType.getSerializer(this) as MySQLSerializer<*> + savedUUIDs.addAll(serializer.getSavedUUIDs()) + } + + return savedUUIDs + } + + private abstract inner class MySQLSerializer : DataTypeSerializer() { + protected abstract val table: ProfileTable + + init { + transaction(database) { + SchemaUtils.create(table) + } + } + + fun getSavedUUIDs(): Set { + return transaction(database) { + table.selectAll().map { it[table.uuid] }.toSet() + } + } + } + + // T is the key type + // S is the stored value type + private abstract inner class SingleValueSerializer : MySQLSerializer() { + abstract override val table: KeyTable + + abstract fun convertToStored(value: T): S + abstract fun convertFromStored(value: S): T + + override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { + val stored = transaction(database) { + table.select { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + .limit(1) + .singleOrNull() + ?.get(table.value) + } + + return stored?.let { convertFromStored(it) } + } + + override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { + transaction(database) { + table.insert { + it[table.uuid] = uuid + it[table.key] = key.key.toString() + it[table.value] = convertToStored(value) + } + } + } + } + + private abstract inner class DirectStoreSerializer : SingleValueSerializer() { + override fun convertToStored(value: T): T { + return value + } + + override fun convertFromStored(value: T): T { + return value + } + } + + private abstract inner class MultiValueSerializer : MySQLSerializer>() { + abstract override val table: ListKeyTable + + override fun readAsync(uuid: UUID, key: PersistentDataKey>): List? { + val stored = transaction(database) { + table.select { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + .orderBy(table.index) + .map { it[table.value] } + } + + return stored + } + + override fun writeAsync(uuid: UUID, key: PersistentDataKey>, value: List) { + transaction(database) { + table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + + value.forEachIndexed { index, t -> + table.insert { + it[table.uuid] = uuid + it[table.key] = key.key.toString() + it[table.index] = index + it[table.value] = t + } + } + } + } + } + + private abstract inner class ProfileTable(name: String) : Table(prefix + name) { + val uuid = uuid("uuid") + } + + private abstract inner class KeyTable(name: String) : ProfileTable(name) { + val key = varchar("key", 128) + abstract val value: Column + + override val primaryKey = PrimaryKey(uuid, key) + + init { + uniqueIndex() + } + } + + private abstract inner class ListKeyTable(name: String) : ProfileTable(name) { + val key = varchar("key", 128) + val index = integer("index") + abstract val value: Column + + override val primaryKey = PrimaryKey(uuid, key, index) + + init { + uniqueIndex() + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt index 7b10d4bc1..4a1b3e1f4 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt @@ -1,10 +1,13 @@ package com.willfp.eco.internal.spigot.data.handlers +import com.willfp.eco.core.config.interfaces.Config +import com.willfp.eco.core.data.handlers.DataTypeSerializer import com.willfp.eco.core.data.handlers.PersistentDataHandler import com.willfp.eco.core.data.handlers.SerializedProfile import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import java.math.BigDecimal import java.util.UUID class YamlPersistentDataHandler( @@ -12,52 +15,59 @@ class YamlPersistentDataHandler( ) : PersistentDataHandler("yaml") { private val dataYml = plugin.dataYml - @Suppress("UNCHECKED_CAST") - override fun read(uuid: UUID, key: PersistentDataKey): T? { - // Separate `as T?` for each branch to prevent compiler warnings. - val value = when (key.type) { - PersistentDataKeyType.INT -> dataYml.getIntOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.DOUBLE -> dataYml.getDoubleOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.STRING -> dataYml.getStringOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.BOOLEAN -> dataYml.getBoolOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.STRING_LIST -> dataYml.getStringsOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.CONFIG -> dataYml.getSubsectionOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.BIG_DECIMAL -> dataYml.getBigDecimalOrNull("player.$uuid.${key.key}") as T? - - else -> null - } + init { + PersistentDataKeyType.STRING.registerSerializer(this, object : YamlSerializer() { + override fun read(config: Config, key: String) = config.getStringOrNull(key) + }) + + PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : YamlSerializer() { + override fun read(config: Config, key: String) = config.getBoolOrNull(key) + }) + + PersistentDataKeyType.INT.registerSerializer(this, object : YamlSerializer() { + override fun read(config: Config, key: String) = config.getIntOrNull(key) + }) + + PersistentDataKeyType.DOUBLE.registerSerializer(this, object : YamlSerializer() { + override fun read(config: Config, key: String) = config.getDoubleOrNull(key) + }) + + PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : YamlSerializer>() { + override fun read(config: Config, key: String) = config.getStringsOrNull(key) + }) - return value + PersistentDataKeyType.CONFIG.registerSerializer(this, object : YamlSerializer() { + override fun read(config: Config, key: String) = config.getSubsectionOrNull(key) + }) + + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : YamlSerializer() { + override fun read(config: Config, key: String) = config.getBigDecimalOrNull(key) + }) } - override fun write(uuid: UUID, key: PersistentDataKey, value: T) { - dataYml.set("player.$uuid.$key", value) + override fun getSavedUUIDs(): Set { + return dataYml.getSubsection("player").getKeys(false) + .map { UUID.fromString(it) } + .toSet() } - override fun serializeData(keys: Set>): Set { - val profiles = mutableSetOf() - val uuids = dataYml.getSubsection("player").getKeys(false).map { UUID.fromString(it) } + override fun shouldAutosave(): Boolean { + return true + } - for (uuid in uuids) { - val data = mutableMapOf, Any>() + override fun doSave() { + dataYml.save() + } - for (key in keys) { - data[key] = read(uuid, key) ?: continue - } + private abstract inner class YamlSerializer: DataTypeSerializer() { + protected abstract fun read(config: Config, key: String): T? - profiles.add(SerializedProfile(uuid, data)) + final override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { + return read(dataYml, "player.$uuid.${key.key}") } - return profiles - } - - override fun loadProfileData(data: Set) { - for (profile in data) { - for ((key, value) in profile.data) { - // Dirty cast, but it's fine because we know it's the same type - @Suppress("UNCHECKED_CAST") - write(profile.uuid, key as PersistentDataKey, value as Any) - } + final override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { + dataYml.set("player.$uuid.${key.key}", value) } } } diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index 92feac624..206a827a1 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -20,12 +20,13 @@ mongodb: database: "eco" mysql: - # How many threads to execute statements on. Higher numbers can be faster however - # very high numbers can cause issues with OS configuration. If writes are taking - # too long, increase this value. - threads: 2 + # The table prefix to use for all tables. + prefix: "eco_" + # The maximum number of MySQL connections. connections: 10 + + # Connection details for MySQL. host: localhost port: 3306 database: database From e87b7ceb77eafe3fa272bfd64a44c63e6cc35d3f Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sat, 24 Aug 2024 17:46:08 +0100 Subject: [PATCH 03/10] Implemented new data backend --- .../data/handlers/DataTypeSerializer.java | 1 - .../data/handlers/PersistentDataHandler.java | 46 ++--- .../data/handlers/PersistentDataHandlers.java | 42 ---- .../com/willfp/eco/internal/spigot/EcoImpl.kt | 15 +- .../eco/internal/spigot/EcoSpigotPlugin.kt | 20 +- .../eco/internal/spigot/data/EcoProfile.kt | 110 ----------- .../eco/internal/spigot/data/KeyRegistry.kt | 4 +- .../internal/spigot/data/ProfileHandler.kt | 185 ------------------ .../data/handlers/PersistentDataHandlers.kt | 34 ++++ .../LegacyMySQLPersistentDataHandler.kt | 24 +-- .../{ => impl}/MongoPersistentDataHandler.kt | 13 +- .../{ => impl}/MySQLPersistentDataHandler.kt | 11 +- .../{ => impl}/YamlPersistentDataHandler.kt | 3 +- .../spigot/data/profiles/ProfileHandler.kt | 126 ++++++++++++ .../ProfileLoadListener.kt} | 18 +- .../spigot/data/profiles/ProfileWriter.kt | 60 ++++++ .../data/profiles/impl/EcoPlayerProfile.kt | 14 ++ .../spigot/data/profiles/impl/EcoProfile.kt | 48 +++++ .../data/profiles/impl/EcoServerProfile.kt | 47 +++++ .../spigot/data/storage/DataHandler.kt | 37 ---- .../spigot/data/storage/HandlerType.kt | 7 - .../spigot/data/storage/MongoDataHandler.kt | 131 ------------- .../spigot/data/storage/MySQLDataHandler.kt | 169 ---------------- .../spigot/data/storage/ProfileSaver.kt | 27 --- .../spigot/data/storage/YamlDataHandler.kt | 67 ------- .../core-plugin/src/main/resources/config.yml | 7 +- 26 files changed, 387 insertions(+), 879 deletions(-) delete mode 100644 eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/EcoProfile.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/ProfileHandler.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt rename eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/{ => impl}/LegacyMySQLPersistentDataHandler.kt (85%) rename eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/{ => impl}/MongoPersistentDataHandler.kt (91%) rename eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/{ => impl}/MySQLPersistentDataHandler.kt (94%) rename eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/{ => impl}/YamlPersistentDataHandler.kt (96%) create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt rename eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/{DataListener.kt => profiles/ProfileLoadListener.kt} (71%) create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoPlayerProfile.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoProfile.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoServerProfile.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/DataHandler.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/HandlerType.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MySQLDataHandler.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/ProfileSaver.kt delete mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/YamlDataHandler.kt diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java index 840f5e2ec..78d543879 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java @@ -1,7 +1,6 @@ package com.willfp.eco.core.data.handlers; import com.willfp.eco.core.data.keys.PersistentDataKey; -import com.willfp.eco.core.data.keys.PersistentDataKeyType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java index a74d156b0..7e8711cb4 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java @@ -16,9 +16,12 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +/** + * Handles persistent data. + */ public abstract class PersistentDataHandler implements Registrable { /** - * The id of the handler. + * The id. */ private final String id; @@ -30,7 +33,7 @@ public abstract class PersistentDataHandler implements Registrable { /** * Create a new persistent data handler. * - * @param id The id of the handler. + * @param id The id. */ protected PersistentDataHandler(@NotNull final String id) { this.id = id; @@ -134,18 +137,16 @@ public final Set serializeData(@NotNull final Set data) { - for (SerializedProfile profile : data) { - for (Map.Entry, Object> entry : profile.data().entrySet()) { - PersistentDataKey key = entry.getKey(); - Object value = entry.getValue(); - - // This cast is safe because the data is serialized - write(profile.uuid(), (PersistentDataKey) key, value); - } + public final void loadSerializedProfile(@NotNull final SerializedProfile profile) { + for (Map.Entry, Object> entry : profile.data().entrySet()) { + PersistentDataKey key = entry.getKey(); + Object value = entry.getValue(); + + // This cast is safe because the data is serialized + write(profile.uuid(), (PersistentDataKey) key, value); } } @@ -153,7 +154,7 @@ public final void loadProfileData(@NotNull Set data) { * Await outstanding writes. */ public final void awaitOutstandingWrites() throws InterruptedException { - boolean success = executor.awaitTermination(15, TimeUnit.SECONDS); + boolean success = executor.awaitTermination(2, TimeUnit.MINUTES); if (!success) { throw new InterruptedException("Failed to await outstanding writes"); @@ -161,27 +162,22 @@ public final void awaitOutstandingWrites() throws InterruptedException { } @Override - public final @NotNull String getID() { + @NotNull + public final String getID() { return id; } @Override - public final boolean equals(@Nullable final Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { + public boolean equals(@NotNull final Object obj) { + if (!(obj instanceof PersistentDataHandler other)) { return false; } - PersistentDataHandler that = (PersistentDataHandler) obj; - - return id.equals(that.id); + return other.getClass().equals(this.getClass()); } @Override - public final int hashCode() { - return id.hashCode(); + public int hashCode() { + return this.getClass().hashCode(); } } diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java deleted file mode 100644 index 6f447ac23..000000000 --- a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandlers.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.willfp.eco.core.data.handlers; - -import com.willfp.eco.core.registry.Registry; -import org.jetbrains.annotations.NotNull; - -/** - * Utility class to manage persistent data handlers. - */ -public final class PersistentDataHandlers { - private static final Registry REGISTRY = new Registry<>(); - - /** - * Register a persistent data handler. - * - * @param handler The handler. - */ - public static void register(@NotNull final PersistentDataHandler handler) { - REGISTRY.register(handler); - } - - /** - * Get a persistent data handler by id. - * - * @param id The id. - * @return The handler. - * @throws IllegalArgumentException if no handler with that id is found. - */ - @NotNull - public static PersistentDataHandler get(@NotNull final String id) { - PersistentDataHandler handler = REGISTRY.get(id); - - if (handler == null) { - throw new IllegalArgumentException("No handler with id: " + id); - } - - return handler; - } - - private PersistentDataHandlers() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); - } -} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt index 4190db5f9..405700485 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt @@ -4,7 +4,6 @@ import com.willfp.eco.core.Eco import com.willfp.eco.core.EcoPlugin import com.willfp.eco.core.PluginLike import com.willfp.eco.core.PluginProps -import com.willfp.eco.core.Prerequisite import com.willfp.eco.core.command.CommandBase import com.willfp.eco.core.command.PluginCommandBase import com.willfp.eco.core.config.ConfigType @@ -44,8 +43,7 @@ import com.willfp.eco.internal.proxy.EcoProxyFactory import com.willfp.eco.internal.scheduling.EcoScheduler import com.willfp.eco.internal.spigot.data.DataYml import com.willfp.eco.internal.spigot.data.KeyRegistry -import com.willfp.eco.internal.spigot.data.ProfileHandler -import com.willfp.eco.internal.spigot.data.storage.HandlerType +import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler import com.willfp.eco.internal.spigot.integrations.bstats.MetricHandler import com.willfp.eco.internal.spigot.math.DelegatedExpressionHandler import com.willfp.eco.internal.spigot.math.ImmediatePlaceholderTranslationExpressionHandler @@ -74,7 +72,7 @@ import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.SkullMeta import org.bukkit.persistence.PersistentDataContainer import java.net.URLClassLoader -import java.util.* +import java.util.UUID private val loadedEcoPlugins = mutableMapOf() @@ -82,10 +80,7 @@ private val loadedEcoPlugins = mutableMapOf() class EcoImpl : EcoSpigotPlugin(), Eco { override val dataYml = DataYml(this) - override val profileHandler = ProfileHandler( - HandlerType.valueOf(this.configYml.getString("data-handler").uppercase()), - this - ) + override val profileHandler = ProfileHandler(this) init { getProxy(CommonsInitializerProxy::class.java).init(this) @@ -290,10 +285,10 @@ class EcoImpl : EcoSpigotPlugin(), Eco { bukkitAudiences override fun getServerProfile() = - profileHandler.loadServerProfile() + profileHandler.getServerProfile() override fun loadPlayerProfile(uuid: UUID) = - profileHandler.load(uuid) + profileHandler.getPlayerProfile(uuid) override fun createDummyEntity(location: Location): Entity = getProxy(DummyEntityFactoryProxy::class.java).createDummyEntity(location) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt index cc0fc8cfb..33a0c5ced 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt @@ -17,7 +17,6 @@ import com.willfp.eco.core.integrations.mcmmo.McmmoManager import com.willfp.eco.core.integrations.placeholder.PlaceholderManager import com.willfp.eco.core.integrations.shop.ShopManager import com.willfp.eco.core.items.Items -import com.willfp.eco.core.items.tag.VanillaItemTag import com.willfp.eco.core.packet.PacketListener import com.willfp.eco.core.particle.Particles import com.willfp.eco.core.price.Prices @@ -62,11 +61,10 @@ import com.willfp.eco.internal.price.PriceFactoryXP import com.willfp.eco.internal.price.PriceFactoryXPLevels import com.willfp.eco.internal.recipes.AutocrafterPatch import com.willfp.eco.internal.spigot.arrows.ArrowDataListener -import com.willfp.eco.internal.spigot.data.DataListener import com.willfp.eco.internal.spigot.data.DataYml import com.willfp.eco.internal.spigot.data.PlayerBlockListener -import com.willfp.eco.internal.spigot.data.ProfileHandler -import com.willfp.eco.internal.spigot.data.storage.ProfileSaver +import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler +import com.willfp.eco.internal.spigot.data.profiles.ProfileLoadListener import com.willfp.eco.internal.spigot.drops.CollatedRunnable import com.willfp.eco.internal.spigot.eventlisteners.EntityDeathByEntityListeners import com.willfp.eco.internal.spigot.eventlisteners.NaturalExpGainListenersPaper @@ -259,9 +257,6 @@ abstract class EcoSpigotPlugin : EcoPlugin() { // Init FIS this.getProxy(FastItemStackFactoryProxy::class.java).create(ItemStack(Material.AIR)).unwrap() - // Preload categorized persistent data keys - profileHandler.initialize() - // Init adventure if (!Prerequisite.HAS_PAPER.isMet) { bukkitAudiences = BukkitAudiences.create(this) @@ -282,14 +277,11 @@ abstract class EcoSpigotPlugin : EcoPlugin() { override fun createTasks() { CollatedRunnable(this) - this.scheduler.runLater(3) { - profileHandler.migrateIfNeeded() + if (!profileHandler.migrateIfNecessary()) { + profileHandler.profileWriter.startTickingAutosave() + profileHandler.profileWriter.startTickingSaves() } - profileHandler.startAutosaving() - - ProfileSaver(this, profileHandler).startTicking() - this.scheduler.runTimer( this.configYml.getInt("display-frame-ttl").toLong(), this.configYml.getInt("display-frame-ttl").toLong(), @@ -428,7 +420,7 @@ abstract class EcoSpigotPlugin : EcoPlugin() { GUIListener(this), ArrowDataListener(this), ArmorChangeEventListeners(this), - DataListener(this, profileHandler), + ProfileLoadListener(this, profileHandler), PlayerBlockListener(this), ServerLocking ) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/EcoProfile.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/EcoProfile.kt deleted file mode 100644 index f8331604a..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/EcoProfile.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.willfp.eco.internal.spigot.data - -import com.willfp.eco.core.EcoPlugin -import com.willfp.eco.core.data.PlayerProfile -import com.willfp.eco.core.data.Profile -import com.willfp.eco.core.data.ServerProfile -import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.core.data.keys.PersistentDataKeyType -import com.willfp.eco.internal.spigot.data.storage.DataHandler -import com.willfp.eco.util.namespacedKeyOf -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap - -abstract class EcoProfile( - val data: MutableMap, Any>, - val uuid: UUID, - private val handler: DataHandler, - private val localHandler: DataHandler -) : Profile { - override fun write(key: PersistentDataKey, value: T) { - this.data[key] = value - - CHANGE_MAP.add(uuid) - } - - override fun read(key: PersistentDataKey): T { - @Suppress("UNCHECKED_CAST") - if (this.data.containsKey(key)) { - return this.data[key] as T - } - - this.data[key] = if (key.isSavedLocally) { - localHandler.read(uuid, key) - } else { - handler.read(uuid, key) - } ?: key.defaultValue - - return read(key) - } - - override fun equals(other: Any?): Boolean { - if (other !is EcoProfile) { - return false - } - - return this.uuid == other.uuid - } - - override fun hashCode(): Int { - return this.uuid.hashCode() - } - - companion object { - val CHANGE_MAP: MutableSet = ConcurrentHashMap.newKeySet() - } -} - -class EcoPlayerProfile( - data: MutableMap, Any>, - uuid: UUID, - handler: DataHandler, - localHandler: DataHandler -) : EcoProfile(data, uuid, handler, localHandler), PlayerProfile { - override fun toString(): String { - return "EcoPlayerProfile{uuid=$uuid}" - } -} - -private val serverIDKey = PersistentDataKey( - namespacedKeyOf("eco", "server_id"), - PersistentDataKeyType.STRING, - "" -) - -private val localServerIDKey = PersistentDataKey( - namespacedKeyOf("eco", "local_server_id"), - PersistentDataKeyType.STRING, - "" -) - -class EcoServerProfile( - data: MutableMap, Any>, - handler: DataHandler, - localHandler: DataHandler -) : EcoProfile(data, serverProfileUUID, handler, localHandler), ServerProfile { - override fun getServerID(): String { - if (this.read(serverIDKey).isBlank()) { - this.write(serverIDKey, UUID.randomUUID().toString()) - } - - return this.read(serverIDKey) - } - - override fun getLocalServerID(): String { - if (this.read(localServerIDKey).isBlank()) { - this.write(localServerIDKey, UUID.randomUUID().toString()) - } - - return this.read(localServerIDKey) - } - - override fun toString(): String { - return "EcoServerProfile" - } -} - -private val PersistentDataKey<*>.isSavedLocally: Boolean - get() = this == localServerIDKey - || EcoPlugin.getPlugin(this.key.namespace)?.isUsingLocalStorage == true - || this.isLocal diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt index 0e331d2d1..129e22fde 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt @@ -19,8 +19,8 @@ object KeyRegistry { this.registry[key.key] = key } - fun getRegisteredKeys(): MutableSet> { - return registry.values.toMutableSet() + fun getRegisteredKeys(): Set> { + return registry.values.toSet() } private fun validateKey(key: PersistentDataKey) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/ProfileHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/ProfileHandler.kt deleted file mode 100644 index 0809ee127..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/ProfileHandler.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.willfp.eco.internal.spigot.data - -import com.willfp.eco.core.data.PlayerProfile -import com.willfp.eco.core.data.Profile -import com.willfp.eco.core.data.ServerProfile -import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.core.data.profile -import com.willfp.eco.internal.spigot.EcoSpigotPlugin -import com.willfp.eco.internal.spigot.ServerLocking -import com.willfp.eco.internal.spigot.data.storage.DataHandler -import com.willfp.eco.internal.spigot.data.storage.HandlerType -import com.willfp.eco.internal.spigot.data.storage.MongoDataHandler -import com.willfp.eco.internal.spigot.data.storage.MySQLDataHandler -import com.willfp.eco.internal.spigot.data.storage.YamlDataHandler -import org.bukkit.Bukkit -import java.util.UUID - -val serverProfileUUID = UUID(0, 0) - -class ProfileHandler( - private val type: HandlerType, - private val plugin: EcoSpigotPlugin -) { - private val loaded = mutableMapOf() - - private val localHandler = YamlDataHandler(plugin, this) - - val handler: DataHandler = when (type) { - HandlerType.YAML -> localHandler - HandlerType.MYSQL -> MySQLDataHandler(plugin, this) - HandlerType.MONGO -> MongoDataHandler(plugin, this) - } - - fun accessLoadedProfile(uuid: UUID): EcoProfile? = - loaded[uuid] - - fun loadGenericProfile(uuid: UUID): Profile { - val found = loaded[uuid] - if (found != null) { - return found - } - - val data = mutableMapOf, Any>() - - val profile = if (uuid == serverProfileUUID) - EcoServerProfile(data, handler, localHandler) else EcoPlayerProfile(data, uuid, handler, localHandler) - - loaded[uuid] = profile - return profile - } - - fun load(uuid: UUID): PlayerProfile { - return loadGenericProfile(uuid) as PlayerProfile - } - - fun loadServerProfile(): ServerProfile { - return loadGenericProfile(serverProfileUUID) as ServerProfile - } - - fun saveKeysFor(uuid: UUID, keys: Set>) { - val profile = accessLoadedProfile(uuid) ?: return - val map = mutableMapOf, Any>() - - for (key in keys) { - map[key] = profile.data[key] ?: continue - } - - handler.saveKeysFor(uuid, map) - - // Don't save to local handler if it's the same handler. - if (localHandler != handler) { - localHandler.saveKeysFor(uuid, map) - } - } - - fun unloadPlayer(uuid: UUID) { - loaded.remove(uuid) - } - - fun save() { - handler.save() - - if (localHandler != handler) { - localHandler.save() - } - } - - fun migrateIfNeeded() { - if (!plugin.configYml.getBool("perform-data-migration")) { - return - } - - if (!plugin.dataYml.has("previous-handler")) { - plugin.dataYml.set("previous-handler", type.name) - plugin.dataYml.save() - } - - - val previousHandlerType = HandlerType.valueOf(plugin.dataYml.getString("previous-handler")) - - if (previousHandlerType == type) { - return - } - - val previousHandler = when (previousHandlerType) { - HandlerType.YAML -> YamlDataHandler(plugin, this) - HandlerType.MYSQL -> MySQLDataHandler(plugin, this) - HandlerType.MONGO -> MongoDataHandler(plugin, this) - } - - ServerLocking.lock("Migrating player data! Check console for more information.") - - plugin.logger.info("eco has detected a change in data handler!") - plugin.logger.info("Migrating server data from ${previousHandlerType.name} to ${type.name}") - plugin.logger.info("This will take a while!") - - plugin.logger.info("Initializing previous handler...") - previousHandler.initialize() - - val players = Bukkit.getOfflinePlayers().map { it.uniqueId } - - plugin.logger.info("Found data for ${players.size} players!") - - /* - Declared here as its own function to be able to use T. - */ - fun migrateKey(uuid: UUID, key: PersistentDataKey, from: DataHandler, to: DataHandler) { - val previous: T? = from.read(uuid, key) - if (previous != null) { - Bukkit.getOfflinePlayer(uuid).profile.write(key, previous) // Nope, no idea. - to.write(uuid, key, previous) - } - } - - var i = 1 - for (uuid in players) { - plugin.logger.info("Migrating data for $uuid... ($i / ${players.size})") - for (key in PersistentDataKey.values()) { - // Why this? Because known points *really* likes to break things with the legacy MySQL handler. - if (key.key.key == "known_points") { - continue - } - - try { - migrateKey(uuid, key, previousHandler, handler) - } catch (e: Exception) { - plugin.logger.info("Could not migrate ${key.key} for $uuid! This is probably because they do not have any data.") - } - } - - i++ - } - - plugin.logger.info("Saving new data...") - handler.save() - plugin.logger.info("Updating previous handler...") - plugin.dataYml.set("previous-handler", type.name) - plugin.dataYml.save() - plugin.logger.info("The server will now automatically be restarted...") - - ServerLocking.unlock() - - Bukkit.getServer().shutdown() - } - - fun initialize() { - handler.initialize() - if (localHandler != handler) { - localHandler.initialize() - } - } - - fun startAutosaving() { - if (!plugin.configYml.getBool("yaml.autosave")) { - return - } - - val interval = plugin.configYml.getInt("yaml.autosave-interval") * 20L - - plugin.scheduler.runTimer(20, interval) { - handler.saveAsync() - localHandler.saveAsync() - } - } -} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt new file mode 100644 index 000000000..c3458fd22 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt @@ -0,0 +1,34 @@ +package com.willfp.eco.internal.spigot.data.handlers + +import com.willfp.eco.core.data.handlers.PersistentDataHandler +import com.willfp.eco.core.registry.KRegistrable +import com.willfp.eco.core.registry.Registry +import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import com.willfp.eco.internal.spigot.data.handlers.impl.MongoPersistentDataHandler +import com.willfp.eco.internal.spigot.data.handlers.impl.MySQLPersistentDataHandler +import com.willfp.eco.internal.spigot.data.handlers.impl.YamlPersistentDataHandler + +abstract class PersistentDataHandlerFactory( + override val id: String +): KRegistrable { + abstract fun create(plugin: EcoSpigotPlugin): PersistentDataHandler +} + +object PersistentDataHandlers: Registry() { + init { + register(object : PersistentDataHandlerFactory("yaml") { + override fun create(plugin: EcoSpigotPlugin) = + YamlPersistentDataHandler(plugin) + }) + + register(object : PersistentDataHandlerFactory("mysql") { + override fun create(plugin: EcoSpigotPlugin) = + MySQLPersistentDataHandler(plugin, plugin.configYml.getSubsection("mysql")) + }) + + register(object : PersistentDataHandlerFactory("mongo") { + override fun create(plugin: EcoSpigotPlugin) = + MongoPersistentDataHandler(plugin, plugin.configYml.getSubsection("mongodb")) + }) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt similarity index 85% rename from eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt rename to eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt index 0672c8b1b..d57574cfc 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/LegacyMySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt @@ -1,7 +1,6 @@ -package com.willfp.eco.internal.spigot.data.handlers +package com.willfp.eco.internal.spigot.data.handlers.impl import com.willfp.eco.core.config.ConfigType -import com.willfp.eco.core.config.Configs import com.willfp.eco.core.config.interfaces.Config import com.willfp.eco.core.config.readConfig import com.willfp.eco.core.data.handlers.DataTypeSerializer @@ -9,23 +8,12 @@ import com.willfp.eco.core.data.handlers.PersistentDataHandler import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlerFactory import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource -import eu.decentsoftware.holograms.api.utils.scheduler.S -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import org.jetbrains.exposed.dao.id.UUIDTable -import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.Table.Dual.decimal -import org.jetbrains.exposed.sql.Table.Dual.double -import org.jetbrains.exposed.sql.Table.Dual.varchar -import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.deleteWhere -import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction @@ -35,7 +23,7 @@ import java.util.UUID class LegacyMySQLPersistentDataHandler( plugin: EcoSpigotPlugin, config: Config -) : PersistentDataHandler("mysql_legacy") { +) : PersistentDataHandler("legacy_mysql") { private val dataSource = HikariDataSource(HikariConfig().apply { driverClassName = "com.mysql.cj.jdbc.Driver" username = config.getString("user") @@ -110,4 +98,10 @@ class LegacyMySQLPersistentDataHandler( throw UnsupportedOperationException("Legacy MySQL does not support writing") } } + + object Factory: PersistentDataHandlerFactory("legacy_mysql") { + override fun create(plugin: EcoSpigotPlugin): PersistentDataHandler { + return LegacyMySQLPersistentDataHandler(plugin, plugin.configYml.getSubsection("mysql")) + } + } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt similarity index 91% rename from eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt rename to eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt index ba69d1e6a..2f63deac4 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MongoPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt @@ -1,4 +1,4 @@ -package com.willfp.eco.internal.spigot.data.handlers +package com.willfp.eco.internal.spigot.data.handlers.impl import com.mongodb.client.model.Filters import com.mongodb.client.model.ReplaceOptions @@ -35,16 +35,7 @@ class MongoPersistentDataHandler( PersistentDataKeyType.INT.registerSerializer(this, MongoSerializer()) PersistentDataKeyType.DOUBLE.registerSerializer(this, MongoSerializer()) PersistentDataKeyType.STRING_LIST.registerSerializer(this, MongoSerializer>()) - - PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : MongoSerializer() { - override fun convertToMongo(value: BigDecimal): Any { - return value.toString() - } - - override fun convertFromMongo(value: Any): BigDecimal { - return BigDecimal(value.toString()) - } - }) + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, MongoSerializer()) PersistentDataKeyType.CONFIG.registerSerializer(this, object : MongoSerializer() { override fun convertToMongo(value: Config): Any { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt similarity index 94% rename from eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt rename to eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index 10f1edd37..5ff9a4b76 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -1,4 +1,4 @@ -package com.willfp.eco.internal.spigot.data.handlers +package com.willfp.eco.internal.spigot.data.handlers.impl import com.willfp.eco.core.config.ConfigType import com.willfp.eco.core.config.Configs @@ -11,19 +11,10 @@ import com.willfp.eco.core.data.keys.PersistentDataKeyType import com.willfp.eco.internal.spigot.EcoSpigotPlugin import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource -import eu.decentsoftware.holograms.api.utils.scheduler.S -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.TextColumnType import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/YamlPersistentDataHandler.kt similarity index 96% rename from eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt rename to eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/YamlPersistentDataHandler.kt index 4a1b3e1f4..7eaa929f8 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/YamlPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/YamlPersistentDataHandler.kt @@ -1,9 +1,8 @@ -package com.willfp.eco.internal.spigot.data.handlers +package com.willfp.eco.internal.spigot.data.handlers.impl import com.willfp.eco.core.config.interfaces.Config import com.willfp.eco.core.data.handlers.DataTypeSerializer import com.willfp.eco.core.data.handlers.PersistentDataHandler -import com.willfp.eco.core.data.handlers.SerializedProfile import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType import com.willfp.eco.internal.spigot.EcoSpigotPlugin diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt new file mode 100644 index 000000000..9b9c64696 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt @@ -0,0 +1,126 @@ +package com.willfp.eco.internal.spigot.data.profiles + +import com.willfp.eco.internal.spigot.EcoSpigotPlugin +import com.willfp.eco.internal.spigot.ServerLocking +import com.willfp.eco.internal.spigot.data.KeyRegistry +import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlerFactory +import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlers +import com.willfp.eco.internal.spigot.data.handlers.impl.LegacyMySQLPersistentDataHandler +import com.willfp.eco.internal.spigot.data.handlers.impl.YamlPersistentDataHandler +import com.willfp.eco.internal.spigot.data.profiles.impl.EcoPlayerProfile +import com.willfp.eco.internal.spigot.data.profiles.impl.EcoProfile +import com.willfp.eco.internal.spigot.data.profiles.impl.EcoServerProfile +import com.willfp.eco.internal.spigot.data.profiles.impl.serverProfileUUID +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +class ProfileHandler( + private val plugin: EcoSpigotPlugin +) { + private val handlerId = plugin.dataYml.getString("data-handler") + + val localHandler = YamlPersistentDataHandler(plugin) + val defaultHandler = PersistentDataHandlers[handlerId] + ?.create(plugin) ?: throw IllegalArgumentException("Invalid data handler ($handlerId)") + + val profileWriter = ProfileWriter(plugin, this) + + private val loaded = ConcurrentHashMap() + + fun getPlayerProfile(uuid: UUID): EcoPlayerProfile { + return loaded.computeIfAbsent(uuid) { + EcoPlayerProfile(it, this) + } as EcoPlayerProfile + } + + fun getServerProfile(): EcoServerProfile { + return loaded.computeIfAbsent(serverProfileUUID) { + EcoServerProfile(this) + } as EcoServerProfile + } + + fun unloadProfile(uuid: UUID) { + loaded.remove(uuid) + } + + fun save() { + localHandler.save() + defaultHandler.save() + + localHandler.awaitOutstandingWrites() + defaultHandler.awaitOutstandingWrites() + } + + fun migrateIfNecessary(): Boolean { + if (!plugin.configYml.getBool("perform-data-migration")) { + return false + } + + if (!plugin.dataYml.has("previous-handler")) { + plugin.dataYml.set("previous-handler", defaultHandler.id) + plugin.dataYml.save() + return false + } + + if (defaultHandler.id == "mysql" && !plugin.dataYml.getBool("legacy-mysql-migrated")) { + plugin.logger.info("eco has detected a legacy MySQL database. Migrating to new MySQL database...") + scheduleMigration(LegacyMySQLPersistentDataHandler.Factory) + + plugin.dataYml.set("legacy-mysql-migrated", true) + plugin.dataYml.save() + + return true + } + + + val previousHandlerId = plugin.dataYml.getString("previous-handler") + if (previousHandlerId != defaultHandler.id) { + val fromFactory = PersistentDataHandlers[previousHandlerId] ?: return false + + scheduleMigration(fromFactory) + + return true + } + + return false + } + + private fun scheduleMigration(fromFactory: PersistentDataHandlerFactory) { + ServerLocking.lock("Migrating player data! Check console for more information.") + + // Run after 5 ticks to allow plugins to load their data keys + plugin.scheduler.runLater(5) { + doMigrate(fromFactory) + } + } + + private fun doMigrate(fromFactory: PersistentDataHandlerFactory) { + plugin.logger.info("eco has detected a change in data handler") + plugin.logger.info("${fromFactory.id} --> $handlerId") + plugin.logger.info("This will take a while! Players will not be able to join during this time.") + + val fromHandler = fromFactory.create(plugin) + val toHandler = defaultHandler + + plugin.logger.info("Loading data from ${fromFactory.id}...") + + val serialized = fromHandler.serializeData(KeyRegistry.getRegisteredKeys()) + + plugin.logger.info("Found ${serialized.size} profiles to migrate") + + for ((index, profile) in serialized.withIndex()) { + plugin.logger.info("(${index + 1}/${serialized.size}) Migrating ${profile.uuid}") + toHandler.loadSerializedProfile(profile) + } + + plugin.logger.info("Profile writes submitted! Waiting for completion...") + toHandler.awaitOutstandingWrites() + + plugin.logger.info("Updating previous handler...") + plugin.dataYml.set("previous-handler", handlerId) + plugin.dataYml.save() + plugin.logger.info("The server will now automatically be restarted...") + + plugin.server.shutdown() + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/DataListener.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileLoadListener.kt similarity index 71% rename from eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/DataListener.kt rename to eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileLoadListener.kt index 36835a3e5..5d461cc55 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/DataListener.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileLoadListener.kt @@ -1,4 +1,4 @@ -package com.willfp.eco.internal.spigot.data +package com.willfp.eco.internal.spigot.data.profiles import com.willfp.eco.core.EcoPlugin import com.willfp.eco.util.PlayerUtils @@ -9,15 +9,18 @@ import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerLoginEvent import org.bukkit.event.player.PlayerQuitEvent -class DataListener( +class ProfileLoadListener( private val plugin: EcoPlugin, private val handler: ProfileHandler ) : Listener { + @EventHandler(priority = EventPriority.LOWEST) + fun onLogin(event: PlayerLoginEvent) { + handler.unloadProfile(event.player.uniqueId) + } + @EventHandler(priority = EventPriority.HIGHEST) fun onLeave(event: PlayerQuitEvent) { - val profile = handler.accessLoadedProfile(event.player.uniqueId) ?: return - handler.saveKeysFor(event.player.uniqueId, profile.data.keys) - handler.unloadPlayer(event.player.uniqueId) + handler.unloadProfile(event.player.uniqueId) } @EventHandler @@ -26,9 +29,4 @@ class DataListener( PlayerUtils.updateSavedDisplayName(event.player) } } - - @EventHandler(priority = EventPriority.LOWEST) - fun onLogin(event: PlayerLoginEvent) { - handler.unloadPlayer(event.player.uniqueId) - } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt new file mode 100644 index 000000000..e8f7dfce0 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt @@ -0,0 +1,60 @@ +package com.willfp.eco.internal.spigot.data.profiles + +import com.willfp.eco.core.EcoPlugin +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.internal.spigot.data.profiles.impl.localServerIDKey +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +/* +The profile writer exists as an optimization to batch writes to the database. + +This is necessary because values frequently change multiple times per tick, +and we don't want to write to the database every time a value changes. + +Instead, we only commit the last value that was set every interval (default 1 tick). + */ + + +class ProfileWriter( + private val plugin: EcoPlugin, + private val handler: ProfileHandler +) { + private val saveInterval = plugin.configYml.getInt("save-interval").toLong() + private val autosaveInterval = plugin.configYml.getInt("autosave-interval").toLong() + private val valuesToWrite = ConcurrentHashMap, Any>() + + fun write(uuid: UUID, key: PersistentDataKey, value: T) { + valuesToWrite[WriteRequest(uuid, key)] = value + } + + fun startTickingSaves() { + plugin.scheduler.runTimer(20, saveInterval) { + val iterator = valuesToWrite.iterator() + + while (iterator.hasNext()) { + val (request, value) = iterator.next() + iterator.remove() + + val dataHandler = if (request.key.isSavedLocally) handler.localHandler else handler.defaultHandler + + // Pass the value to the data handler + @Suppress("UNCHECKED_CAST") + dataHandler.write(request.uuid, request.key as PersistentDataKey, value) + } + } + } + + fun startTickingAutosave() { + plugin.scheduler.runTimer(autosaveInterval, autosaveInterval) { + if (handler.localHandler.shouldAutosave()) { + handler.localHandler.save() + } + } + } + + private data class WriteRequest(val uuid: UUID, val key: PersistentDataKey) +} + +val PersistentDataKey<*>.isSavedLocally: Boolean + get() = this.isLocal || EcoPlugin.getPlugin(this.key.namespace)?.isUsingLocalStorage == true diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoPlayerProfile.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoPlayerProfile.kt new file mode 100644 index 000000000..263db3a14 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoPlayerProfile.kt @@ -0,0 +1,14 @@ +package com.willfp.eco.internal.spigot.data.profiles.impl + +import com.willfp.eco.core.data.PlayerProfile +import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler +import java.util.UUID + +class EcoPlayerProfile( + uuid: UUID, + handler: ProfileHandler +) : EcoProfile(uuid, handler), PlayerProfile { + override fun toString(): String { + return "EcoPlayerProfile{uuid=$uuid}" + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoProfile.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoProfile.kt new file mode 100644 index 000000000..e136e6fbd --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoProfile.kt @@ -0,0 +1,48 @@ +package com.willfp.eco.internal.spigot.data.profiles.impl + +import com.willfp.eco.core.data.Profile +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler +import com.willfp.eco.internal.spigot.data.profiles.isSavedLocally +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +abstract class EcoProfile( + val uuid: UUID, + private val handler: ProfileHandler +) : Profile { + private val data = ConcurrentHashMap, Any>() + + override fun write(key: PersistentDataKey, value: T) { + this.data[key] = value + + handler.profileWriter.write(uuid, key, value) + } + + override fun read(key: PersistentDataKey): T { + @Suppress("UNCHECKED_CAST") + if (this.data.containsKey(key)) { + return this.data[key] as T + } + + this.data[key] = if (key.isSavedLocally) { + handler.localHandler.read(uuid, key) + } else { + handler.defaultHandler.read(uuid, key) + } ?: key.defaultValue + + return read(key) + } + + override fun equals(other: Any?): Boolean { + if (other !is EcoProfile) { + return false + } + + return this.uuid == other.uuid + } + + override fun hashCode(): Int { + return this.uuid.hashCode() + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoServerProfile.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoServerProfile.kt new file mode 100644 index 000000000..4bf4bead6 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/impl/EcoServerProfile.kt @@ -0,0 +1,47 @@ +package com.willfp.eco.internal.spigot.data.profiles.impl + +import com.willfp.eco.core.data.ServerProfile +import com.willfp.eco.core.data.keys.PersistentDataKey +import com.willfp.eco.core.data.keys.PersistentDataKeyType +import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler +import com.willfp.eco.util.namespacedKeyOf +import java.util.UUID + +val serverIDKey = PersistentDataKey( + namespacedKeyOf("eco", "server_id"), + PersistentDataKeyType.STRING, + "" +) + +val localServerIDKey = PersistentDataKey( + namespacedKeyOf("eco", "local_server_id"), + PersistentDataKeyType.STRING, + "", + true +) + +val serverProfileUUID = UUID(0, 0) + +class EcoServerProfile( + handler: ProfileHandler +) : EcoProfile(serverProfileUUID, handler), ServerProfile { + override fun getServerID(): String { + if (this.read(serverIDKey).isBlank()) { + this.write(serverIDKey, UUID.randomUUID().toString()) + } + + return this.read(serverIDKey) + } + + override fun getLocalServerID(): String { + if (this.read(localServerIDKey).isBlank()) { + this.write(localServerIDKey, UUID.randomUUID().toString()) + } + + return this.read(localServerIDKey) + } + + override fun toString(): String { + return "EcoServerProfile" + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/DataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/DataHandler.kt deleted file mode 100644 index 0855a0663..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/DataHandler.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.willfp.eco.internal.spigot.data.storage - -import com.willfp.eco.core.data.keys.PersistentDataKey -import java.util.UUID - -abstract class DataHandler( - val type: HandlerType -) { - /** - * Read value from a key. - */ - abstract fun read(uuid: UUID, key: PersistentDataKey): T? - - /** - * Write value to a key. - */ - abstract fun write(uuid: UUID, key: PersistentDataKey, value: T) - - /** - * Save a set of keys for a given UUID. - */ - abstract fun saveKeysFor(uuid: UUID, keys: Map, Any>) - - // Everything below this are methods that are only needed for certain implementations. - - open fun save() { - - } - - open fun saveAsync() { - - } - - open fun initialize() { - - } -} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/HandlerType.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/HandlerType.kt deleted file mode 100644 index 87534e306..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/HandlerType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.willfp.eco.internal.spigot.data.storage - -enum class HandlerType { - YAML, - MYSQL, - MONGO -} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt deleted file mode 100644 index a00c811bb..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MongoDataHandler.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.willfp.eco.internal.spigot.data.storage - -import com.mongodb.client.model.Filters -import com.mongodb.client.model.ReplaceOptions -import com.mongodb.client.model.Updates -import com.mongodb.kotlin.client.coroutine.MongoClient -import com.mongodb.kotlin.client.coroutine.MongoCollection -import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.internal.spigot.EcoSpigotPlugin -import com.willfp.eco.internal.spigot.data.ProfileHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.util.UUID - -@Suppress("UNCHECKED_CAST") -class MongoDataHandler( - plugin: EcoSpigotPlugin, - private val handler: ProfileHandler -) : DataHandler(HandlerType.MONGO) { - private val client: MongoClient - private val collection: MongoCollection - - private val scope = CoroutineScope(Dispatchers.IO) - - init { - System.setProperty( - "org.litote.mongo.mapping.service", - "org.litote.kmongo.jackson.JacksonClassMappingTypeService" - ) - - val url = plugin.configYml.getString("mongodb.url") - - client = MongoClient.create(url) - collection = client.getDatabase(plugin.configYml.getString("mongodb.database")) - .getCollection("uuidprofile") // Compat with jackson mapping - } - - override fun read(uuid: UUID, key: PersistentDataKey): T? { - return runBlocking { - doRead(uuid, key) - } - } - - override fun write(uuid: UUID, key: PersistentDataKey, value: T) { - scope.launch { - doWrite(uuid, key, value) - } - } - - override fun saveKeysFor(uuid: UUID, keys: Map, Any>) { - scope.launch { - for ((key, value) in keys) { - saveKey(uuid, key, value) - } - } - } - - private suspend fun saveKey(uuid: UUID, key: PersistentDataKey, value: Any) { - val data = value as T - doWrite(uuid, key, data) - } - - private suspend fun doWrite(uuid: UUID, key: PersistentDataKey, value: T) { - val profile = getOrCreateDocument(uuid) - - profile.data.run { - if (value == null) { - this.remove(key.key.toString()) - } else { - this[key.key.toString()] = value - } - } - - collection.updateOne( - Filters.eq(UUIDProfile::uuid.name, uuid.toString()), - Updates.set(UUIDProfile::data.name, profile.data) - ) - } - - private suspend fun doRead(uuid: UUID, key: PersistentDataKey): T? { - val profile = collection.find(Filters.eq(UUIDProfile::uuid.name, uuid.toString())) - .firstOrNull() ?: return key.defaultValue - return profile.data[key.key.toString()] as? T? - } - - private suspend fun getOrCreateDocument(uuid: UUID): UUIDProfile { - val profile = collection.find(Filters.eq(UUIDProfile::uuid.name, uuid.toString())) - .firstOrNull() - return if (profile == null) { - val toInsert = UUIDProfile( - uuid.toString(), - mutableMapOf() - ) - - collection.replaceOne( - Filters.eq(UUIDProfile::uuid.name, uuid.toString()), - toInsert, - ReplaceOptions().upsert(true) - ) - toInsert - } else { - profile - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is MongoDataHandler - } - - override fun hashCode(): Int { - return type.hashCode() - } -} - -@Serializable -internal data class UUIDProfile( - // Storing UUID as strings for serialization - @SerialName("_id") val uuid: String, - // Storing NamespacedKeys as strings for serialization - val data: MutableMap -) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MySQLDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MySQLDataHandler.kt deleted file mode 100644 index b1868548a..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/MySQLDataHandler.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.willfp.eco.internal.spigot.data.storage - -import com.github.benmanes.caffeine.cache.Caffeine -import com.google.common.util.concurrent.ThreadFactoryBuilder -import com.willfp.eco.core.config.ConfigType -import com.willfp.eco.core.config.interfaces.Config -import com.willfp.eco.core.config.readConfig -import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.core.data.keys.PersistentDataKeyType -import com.willfp.eco.internal.spigot.EcoSpigotPlugin -import com.willfp.eco.internal.spigot.data.ProfileHandler -import com.zaxxer.hikari.HikariConfig -import com.zaxxer.hikari.HikariDataSource -import org.jetbrains.exposed.dao.id.UUIDTable -import org.jetbrains.exposed.sql.Column -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.TextColumnType -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.update -import java.util.UUID -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -/* -Better than old MySQL data handler, but that's only because it's literally just dumping all the -data into a single text column, containing the contents of the players profile as a Config. - -Whatever. At least it works. - */ - -@Suppress("UNCHECKED_CAST") -class MySQLDataHandler( - plugin: EcoSpigotPlugin, - private val handler: ProfileHandler -) : DataHandler(HandlerType.MYSQL) { - private val database: Database - private val table = UUIDTable("eco_data") - - private val rows = Caffeine.newBuilder() - .expireAfterWrite(3, TimeUnit.SECONDS) - .build() - - private val threadFactory = ThreadFactoryBuilder().setNameFormat("eco-mysql-thread-%d").build() - private val executor = Executors.newFixedThreadPool(plugin.configYml.getInt("mysql.threads"), threadFactory) - - private val dataColumn: Column - get() = table.columns.first { it.name == "json_data" } as Column - - init { - val config = HikariConfig() - config.driverClassName = "com.mysql.cj.jdbc.Driver" - config.username = plugin.configYml.getString("mysql.user") - config.password = plugin.configYml.getString("mysql.password") - config.jdbcUrl = "jdbc:mysql://" + - "${plugin.configYml.getString("mysql.host")}:" + - "${plugin.configYml.getString("mysql.port")}/" + - plugin.configYml.getString("mysql.database") - config.maximumPoolSize = plugin.configYml.getInt("mysql.connections") - - database = Database.connect(HikariDataSource(config)) - - transaction(database) { - SchemaUtils.create(table) - - table.apply { - registerColumn("json_data", TextColumnType()) - } - - SchemaUtils.createMissingTablesAndColumns(table, withLogs = false) - } - } - - override fun read(uuid: UUID, key: PersistentDataKey): T? { - val data = getData(uuid) - - val value: Any? = when (key.type) { - PersistentDataKeyType.INT -> data.getIntOrNull(key.key.toString()) - PersistentDataKeyType.DOUBLE -> data.getDoubleOrNull(key.key.toString()) - PersistentDataKeyType.STRING -> data.getStringOrNull(key.key.toString()) - PersistentDataKeyType.BOOLEAN -> data.getBoolOrNull(key.key.toString()) - PersistentDataKeyType.STRING_LIST -> data.getStringsOrNull(key.key.toString()) - PersistentDataKeyType.CONFIG -> data.getSubsectionOrNull(key.key.toString()) - PersistentDataKeyType.BIG_DECIMAL -> data.getBigDecimalOrNull(key.key.toString()) - - else -> null - } - - return value as? T? - } - - override fun write(uuid: UUID, key: PersistentDataKey, value: T) { - val data = getData(uuid) - data.set(key.key.toString(), value) - - setData(uuid, data) - } - - override fun saveKeysFor(uuid: UUID, keys: Map, Any>) { - executor.submit { - val data = getData(uuid) - - for ((key, value) in keys) { - data.set(key.key.toString(), value) - } - - doSetData(uuid, data) - } - } - - private fun getData(uuid: UUID): Config { - val plaintext = transaction(database) { - val row = rows.get(uuid) { - val row = table.select { table.id eq uuid }.limit(1).singleOrNull() - - if (row != null) { - row - } else { - transaction(database) { - table.insert { - it[id] = uuid - it[dataColumn] = "{}" - } - } - table.select { table.id eq uuid }.limit(1).singleOrNull() - } - } - - row.getOrNull(dataColumn) ?: "{}" - } - - return readConfig(plaintext, ConfigType.JSON) - } - - private fun setData(uuid: UUID, config: Config) { - executor.submit { - doSetData(uuid, config) - } - } - - private fun doSetData(uuid: UUID, config: Config) { - transaction(database) { - table.update({ table.id eq uuid }) { - it[dataColumn] = config.toPlaintext() - } - } - } - - override fun initialize() { - transaction(database) { - SchemaUtils.createMissingTablesAndColumns(table, withLogs = false) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is MySQLDataHandler - } - - override fun hashCode(): Int { - return type.hashCode() - } -} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/ProfileSaver.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/ProfileSaver.kt deleted file mode 100644 index b44017c87..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/ProfileSaver.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.willfp.eco.internal.spigot.data.storage - -import com.willfp.eco.core.EcoPlugin -import com.willfp.eco.internal.spigot.data.EcoProfile -import com.willfp.eco.internal.spigot.data.ProfileHandler - -class ProfileSaver( - private val plugin: EcoPlugin, - private val handler: ProfileHandler -) { - fun startTicking() { - val interval = plugin.configYml.getInt("save-interval").toLong() - - plugin.scheduler.runTimer(20, interval) { - val iterator = EcoProfile.CHANGE_MAP.iterator() - - while (iterator.hasNext()) { - val uuid = iterator.next() - iterator.remove() - - val profile = handler.accessLoadedProfile(uuid) ?: continue - - handler.saveKeysFor(uuid, profile.data.keys) - } - } - } -} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/YamlDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/YamlDataHandler.kt deleted file mode 100644 index edbf73b61..000000000 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/storage/YamlDataHandler.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.willfp.eco.internal.spigot.data.storage - -import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.core.data.keys.PersistentDataKeyType -import com.willfp.eco.internal.spigot.EcoSpigotPlugin -import com.willfp.eco.internal.spigot.data.ProfileHandler -import org.bukkit.NamespacedKey -import java.util.UUID - -@Suppress("UNCHECKED_CAST") -class YamlDataHandler( - plugin: EcoSpigotPlugin, - private val handler: ProfileHandler -) : DataHandler(HandlerType.YAML) { - private val dataYml = plugin.dataYml - - override fun save() { - dataYml.save() - } - - override fun saveAsync() { - dataYml.saveAsync() - } - - override fun read(uuid: UUID, key: PersistentDataKey): T? { - // Separate `as T?` for each branch to prevent compiler warnings. - val value = when (key.type) { - PersistentDataKeyType.INT -> dataYml.getIntOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.DOUBLE -> dataYml.getDoubleOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.STRING -> dataYml.getStringOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.BOOLEAN -> dataYml.getBoolOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.STRING_LIST -> dataYml.getStringsOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.CONFIG -> dataYml.getSubsectionOrNull("player.$uuid.${key.key}") as T? - PersistentDataKeyType.BIG_DECIMAL -> dataYml.getBigDecimalOrNull("player.$uuid.${key.key}") as T? - - else -> null - } - - return value - } - - override fun write(uuid: UUID, key: PersistentDataKey, value: T) { - doWrite(uuid, key.key, value) - } - - override fun saveKeysFor(uuid: UUID, keys: Map, Any>) { - for ((key, value) in keys) { - doWrite(uuid, key.key, value) - } - } - - private fun doWrite(uuid: UUID, key: NamespacedKey, value: Any) { - dataYml.set("player.$uuid.$key", value) - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is YamlDataHandler - } - - override fun hashCode(): Int { - return type.hashCode() - } -} diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index 206a827a1..cde16fad7 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -33,16 +33,15 @@ mysql: user: username password: passy -yaml: - autosave: true # If data should be saved automatically - autosave-interval: 1800 # How often data should be saved (in seconds) - # How many ticks to wait between committing data to a database. This doesn't # affect yaml storage, only MySQL and MongoDB. By default, data is committed # every tick, but you can increase this to be every x ticks, for example 20 # would be committing once a second. save-interval: 1 +# How many ticks to wait between autosaves for data.yml. +autosave-interval: 36000 # 30 minutes + # Options to manage the conflict finder conflicts: whitelist: # Plugins that should never be marked as conflicts From 84d481d753516ec95fd3497b14d992c9ebf24bfd Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sat, 24 Aug 2024 19:39:54 +0100 Subject: [PATCH 04/10] Fixed several bugs with the new data system --- .../data/handlers/DataTypeSerializer.java | 2 + .../data/handlers/PersistentDataHandler.java | 38 ++++---- .../eco/internal/spigot/EcoSpigotPlugin.kt | 2 +- .../data/handlers/PersistentDataHandlers.kt | 4 +- .../impl/LegacyMySQLPersistentDataHandler.kt | 3 +- .../impl/MongoPersistentDataHandler.kt | 1 - .../impl/MySQLPersistentDataHandler.kt | 88 ++++++++++++------- .../spigot/data/profiles/ProfileHandler.kt | 38 ++++---- .../core-plugin/src/main/resources/config.yml | 4 +- 9 files changed, 98 insertions(+), 82 deletions(-) diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java index 78d543879..3f25946dd 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/DataTypeSerializer.java @@ -9,6 +9,8 @@ /** * Handles data read/write for a {@link com.willfp.eco.core.data.keys.PersistentDataKeyType} for a specific * data handler. + * + * @param The type of data. */ public abstract class DataTypeSerializer { /** diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java index 7e8711cb4..b22bb0175 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/handlers/PersistentDataHandler.java @@ -1,7 +1,9 @@ package com.willfp.eco.core.data.handlers; +import com.willfp.eco.core.Eco; import com.willfp.eco.core.data.keys.PersistentDataKey; import com.willfp.eco.core.registry.Registrable; +import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -125,7 +127,10 @@ public final Set serializeData(@NotNull final Set key : keys) { Object value = read(uuid, key); - data.put(key, value); + + if (value != null) { + data.put(key, value); + } } profiles.add(new SerializedProfile(uuid, data)); @@ -151,13 +156,20 @@ public final void loadSerializedProfile(@NotNull final SerializedProfile profile } /** - * Await outstanding writes. + * Save and shutdown the handler. + * + * @throws InterruptedException If the writes could not be awaited. */ - public final void awaitOutstandingWrites() throws InterruptedException { - boolean success = executor.awaitTermination(2, TimeUnit.MINUTES); + public final void shutdown() throws InterruptedException { + doSave(); + + if (executor.isShutdown()) { + return; + } - if (!success) { - throw new InterruptedException("Failed to await outstanding writes"); + executor.shutdown(); + while (!executor.awaitTermination(2, TimeUnit.MINUTES)) { + // Wait } } @@ -166,18 +178,4 @@ public final void awaitOutstandingWrites() throws InterruptedException { public final String getID() { return id; } - - @Override - public boolean equals(@NotNull final Object obj) { - if (!(obj instanceof PersistentDataHandler other)) { - return false; - } - - return other.getClass().equals(this.getClass()); - } - - @Override - public int hashCode() { - return this.getClass().hashCode(); - } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt index 33a0c5ced..06924ccf0 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt @@ -148,7 +148,7 @@ import org.bukkit.inventory.ItemStack abstract class EcoSpigotPlugin : EcoPlugin() { abstract val dataYml: DataYml - protected abstract val profileHandler: ProfileHandler + abstract val profileHandler: ProfileHandler protected var bukkitAudiences: BukkitAudiences? = null init { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt index c3458fd22..dce85b568 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/PersistentDataHandlers.kt @@ -23,12 +23,12 @@ object PersistentDataHandlers: Registry() { register(object : PersistentDataHandlerFactory("mysql") { override fun create(plugin: EcoSpigotPlugin) = - MySQLPersistentDataHandler(plugin, plugin.configYml.getSubsection("mysql")) + MySQLPersistentDataHandler(plugin.configYml.getSubsection("mysql")) }) register(object : PersistentDataHandlerFactory("mongo") { override fun create(plugin: EcoSpigotPlugin) = - MongoPersistentDataHandler(plugin, plugin.configYml.getSubsection("mongodb")) + MongoPersistentDataHandler(plugin.configYml.getSubsection("mongodb")) }) } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt index d57574cfc..037a61636 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt @@ -21,7 +21,6 @@ import java.math.BigDecimal import java.util.UUID class LegacyMySQLPersistentDataHandler( - plugin: EcoSpigotPlugin, config: Config ) : PersistentDataHandler("legacy_mysql") { private val dataSource = HikariDataSource(HikariConfig().apply { @@ -101,7 +100,7 @@ class LegacyMySQLPersistentDataHandler( object Factory: PersistentDataHandlerFactory("legacy_mysql") { override fun create(plugin: EcoSpigotPlugin): PersistentDataHandler { - return LegacyMySQLPersistentDataHandler(plugin, plugin.configYml.getSubsection("mysql")) + return LegacyMySQLPersistentDataHandler(plugin.configYml.getSubsection("mysql")) } } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt index 2f63deac4..d78894b84 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt @@ -20,7 +20,6 @@ import java.math.BigDecimal import java.util.UUID class MongoPersistentDataHandler( - plugin: EcoSpigotPlugin, config: Config ) : PersistentDataHandler("mongo") { private val client = MongoClient.create(config.getString("url")) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index 5ff9a4b76..13dd0930a 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -8,7 +8,6 @@ import com.willfp.eco.core.data.handlers.DataTypeSerializer import com.willfp.eco.core.data.handlers.PersistentDataHandler import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType -import com.willfp.eco.internal.spigot.EcoSpigotPlugin import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.sql.Column @@ -25,7 +24,6 @@ import java.math.BigDecimal import java.util.UUID class MySQLPersistentDataHandler( - plugin: EcoSpigotPlugin, config: Config ) : PersistentDataHandler("mysql") { private val dataSource = HikariDataSource(HikariConfig().apply { @@ -48,32 +46,32 @@ class MySQLPersistentDataHandler( override val table = object : KeyTable("string") { override val value = varchar("value", 128) } - }) + }.createTable()) PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : DirectStoreSerializer() { override val table = object : KeyTable("boolean") { override val value = bool("value") } - }) + }.createTable()) PersistentDataKeyType.INT.registerSerializer(this, object : DirectStoreSerializer() { override val table = object : KeyTable("int") { override val value = integer("value") } - }) + }.createTable()) PersistentDataKeyType.DOUBLE.registerSerializer(this, object : DirectStoreSerializer() { override val table = object : KeyTable("double") { override val value = double("value") } - }) + }.createTable()) PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : DirectStoreSerializer() { override val table = object : KeyTable("big_decimal") { // 34 digits of precision, 4 digits of scale override val value = decimal("value", 34, 4) } - }) + }.createTable()) PersistentDataKeyType.CONFIG.registerSerializer(this, object : SingleValueSerializer() { override val table = object : KeyTable("config") { @@ -92,13 +90,13 @@ class MySQLPersistentDataHandler( Configs.fromMap(value.toMap(), ConfigType.JSON).toPlaintext() } } - }) + }.createTable()) PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MultiValueSerializer() { override val table = object : ListKeyTable("string_list") { override val value = varchar("value", 128) } - }) + }.createTable()) } override fun getSavedUUIDs(): Set { @@ -115,22 +113,24 @@ class MySQLPersistentDataHandler( private abstract inner class MySQLSerializer : DataTypeSerializer() { protected abstract val table: ProfileTable - init { - transaction(database) { - SchemaUtils.create(table) - } - } - fun getSavedUUIDs(): Set { return transaction(database) { table.selectAll().map { it[table.uuid] }.toSet() } } + + fun createTable(): MySQLSerializer { + transaction(database) { + SchemaUtils.create(table) + } + + return this + } } // T is the key type // S is the stored value type - private abstract inner class SingleValueSerializer : MySQLSerializer() { + private abstract inner class SingleValueSerializer : MySQLSerializer() { abstract override val table: KeyTable abstract fun convertToStored(value: T): S @@ -148,17 +148,21 @@ class MySQLPersistentDataHandler( } override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { - transaction(database) { - table.insert { - it[table.uuid] = uuid - it[table.key] = key.key.toString() - it[table.value] = convertToStored(value) + withRetries { + transaction(database) { + table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + + table.insert { + it[table.uuid] = uuid + it[table.key] = key.key.toString() + it[table.value] = convertToStored(value) + } } } } } - private abstract inner class DirectStoreSerializer : SingleValueSerializer() { + private abstract inner class DirectStoreSerializer : SingleValueSerializer() { override fun convertToStored(value: T): T { return value } @@ -168,7 +172,7 @@ class MySQLPersistentDataHandler( } } - private abstract inner class MultiValueSerializer : MySQLSerializer>() { + private abstract inner class MultiValueSerializer : MySQLSerializer>() { abstract override val table: ListKeyTable override fun readAsync(uuid: UUID, key: PersistentDataKey>): List? { @@ -182,15 +186,17 @@ class MySQLPersistentDataHandler( } override fun writeAsync(uuid: UUID, key: PersistentDataKey>, value: List) { - transaction(database) { - table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } - - value.forEachIndexed { index, t -> - table.insert { - it[table.uuid] = uuid - it[table.key] = key.key.toString() - it[table.index] = index - it[table.value] = t + withRetries { + transaction(database) { + table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + + value.forEachIndexed { index, t -> + table.insert { + it[table.uuid] = uuid + it[table.key] = key.key.toString() + it[table.index] = index + it[table.value] = t + } } } } @@ -208,7 +214,7 @@ class MySQLPersistentDataHandler( override val primaryKey = PrimaryKey(uuid, key) init { - uniqueIndex() + uniqueIndex(uuid, key) } } @@ -220,7 +226,21 @@ class MySQLPersistentDataHandler( override val primaryKey = PrimaryKey(uuid, key, index) init { - uniqueIndex() + uniqueIndex(uuid, key, index) + } + } + + private inline fun withRetries(action: () -> T): T { + var retries = 0 + while (true) { + try { + return action() + } catch (e: Exception) { + if (retries >= 3) { + throw e + } + retries++ + } } } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt index 9b9c64696..fb5b9903e 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt @@ -6,6 +6,7 @@ import com.willfp.eco.internal.spigot.data.KeyRegistry import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlerFactory import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlers import com.willfp.eco.internal.spigot.data.handlers.impl.LegacyMySQLPersistentDataHandler +import com.willfp.eco.internal.spigot.data.handlers.impl.MySQLPersistentDataHandler import com.willfp.eco.internal.spigot.data.handlers.impl.YamlPersistentDataHandler import com.willfp.eco.internal.spigot.data.profiles.impl.EcoPlayerProfile import com.willfp.eco.internal.spigot.data.profiles.impl.EcoProfile @@ -17,7 +18,7 @@ import java.util.concurrent.ConcurrentHashMap class ProfileHandler( private val plugin: EcoSpigotPlugin ) { - private val handlerId = plugin.dataYml.getString("data-handler") + private val handlerId = plugin.configYml.getString("data-handler") val localHandler = YamlPersistentDataHandler(plugin) val defaultHandler = PersistentDataHandlers[handlerId] @@ -44,11 +45,8 @@ class ProfileHandler( } fun save() { - localHandler.save() - defaultHandler.save() - - localHandler.awaitOutstandingWrites() - defaultHandler.awaitOutstandingWrites() + localHandler.shutdown() + defaultHandler.shutdown() } fun migrateIfNecessary(): Boolean { @@ -56,28 +54,25 @@ class ProfileHandler( return false } + // First install if (!plugin.dataYml.has("previous-handler")) { plugin.dataYml.set("previous-handler", defaultHandler.id) + plugin.dataYml.set("legacy-mysql-migrated", true) plugin.dataYml.save() return false } - if (defaultHandler.id == "mysql" && !plugin.dataYml.getBool("legacy-mysql-migrated")) { - plugin.logger.info("eco has detected a legacy MySQL database. Migrating to new MySQL database...") - scheduleMigration(LegacyMySQLPersistentDataHandler.Factory) - - plugin.dataYml.set("legacy-mysql-migrated", true) - plugin.dataYml.save() + val previousHandlerId = plugin.dataYml.getString("previous-handler").lowercase() + if (previousHandlerId != defaultHandler.id) { + val fromFactory = PersistentDataHandlers[previousHandlerId] ?: return false + scheduleMigration(fromFactory) return true } - - val previousHandlerId = plugin.dataYml.getString("previous-handler") - if (previousHandlerId != defaultHandler.id) { - val fromFactory = PersistentDataHandlers[previousHandlerId] ?: return false - - scheduleMigration(fromFactory) + if (defaultHandler is MySQLPersistentDataHandler && !plugin.dataYml.getBool("legacy-mysql-migrated")) { + plugin.logger.info("eco has detected a legacy MySQL database. Migrating to new MySQL database...") + scheduleMigration(LegacyMySQLPersistentDataHandler.Factory) return true } @@ -91,12 +86,15 @@ class ProfileHandler( // Run after 5 ticks to allow plugins to load their data keys plugin.scheduler.runLater(5) { doMigrate(fromFactory) + + plugin.dataYml.set("legacy-mysql-migrated", true) + plugin.dataYml.save() } } private fun doMigrate(fromFactory: PersistentDataHandlerFactory) { plugin.logger.info("eco has detected a change in data handler") - plugin.logger.info("${fromFactory.id} --> $handlerId") + plugin.logger.info("${fromFactory.id} --> ${defaultHandler.id}") plugin.logger.info("This will take a while! Players will not be able to join during this time.") val fromHandler = fromFactory.create(plugin) @@ -114,7 +112,7 @@ class ProfileHandler( } plugin.logger.info("Profile writes submitted! Waiting for completion...") - toHandler.awaitOutstandingWrites() + toHandler.shutdown() plugin.logger.info("Updating previous handler...") plugin.dataYml.set("previous-handler", handlerId) diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index cde16fad7..4a6640f54 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -6,8 +6,8 @@ # How player/server data is saved: # yaml - Stored in data.yml: Good option for single-node servers (i.e. no BungeeCord/Velocity) -# mongo - If you're running on a network (Bungee/Velocity), you should use MongoDB if you can. -# mysql - The alternative to MongoDB. Because of how eco data works, MongoDB is the best option; but use this if you can't. +# mysql - Standard database, great option for multi-node servers (i.e. BungeeCord/Velocity) +# mongo - Alternative database, may suit some servers better data-handler: yaml # If data should be migrated automatically when changing data handler. From 3d78bad4b1bc59de5c78e7f2109dcecff7763f12 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sun, 25 Aug 2024 16:45:38 +0100 Subject: [PATCH 05/10] Implemented new MongoDB handler --- build.gradle.kts | 3 - eco-core/core-plugin/build.gradle.kts | 15 +- .../impl/LegacyMySQLPersistentDataHandler.kt | 8 +- .../impl/MongoPersistentDataHandler.kt | 178 +++++++++++++----- .../impl/MySQLPersistentDataHandler.kt | 25 ++- .../spigot/data/profiles/ProfileHandler.kt | 4 +- .../core-plugin/src/main/resources/config.yml | 6 +- 7 files changed, 170 insertions(+), 69 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1e0b2db73..5bee9a7be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,6 @@ plugins { id("maven-publish") id("java") kotlin("jvm") version "1.9.21" - kotlin("plugin.serialization") version "1.9.21" } dependencies { @@ -41,7 +40,6 @@ allprojects { apply(plugin = "maven-publish") apply(plugin = "io.github.goooler.shadow") apply(plugin = "kotlin") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") repositories { mavenCentral() @@ -212,7 +210,6 @@ tasks { //relocate("com.mysql", "com.willfp.eco.libs.mysql") relocate("com.mongodb", "com.willfp.eco.libs.mongodb") relocate("org.bson", "com.willfp.eco.libs.bson") - relocate("org.litote", "com.willfp.eco.libs.litote") relocate("org.reactivestreams", "com.willfp.eco.libs.reactivestreams") relocate("reactor.", "com.willfp.eco.libs.reactor.") // Dot in name to be safe relocate("com.moandjiezana.toml", "com.willfp.eco.libs.toml") diff --git a/eco-core/core-plugin/build.gradle.kts b/eco-core/core-plugin/build.gradle.kts index 124a48643..7cbb676b9 100644 --- a/eco-core/core-plugin/build.gradle.kts +++ b/eco-core/core-plugin/build.gradle.kts @@ -9,16 +9,14 @@ dependencies { // Libraries implementation("com.github.WillFP:Crunch:1.1.3") - implementation("mysql:mysql-connector-java:8.0.25") - implementation("org.jetbrains.exposed:exposed-core:0.37.3") - implementation("org.jetbrains.exposed:exposed-dao:0.37.3") - implementation("org.jetbrains.exposed:exposed-jdbc:0.37.3") - implementation("com.zaxxer:HikariCP:5.0.0") + implementation("mysql:mysql-connector-java:8.0.28") + implementation("org.jetbrains.exposed:exposed-core:0.53.0") + implementation("org.jetbrains.exposed:exposed-jdbc:0.53.0") + implementation("com.zaxxer:HikariCP:5.1.0") implementation("net.kyori:adventure-platform-bukkit:4.1.0") implementation("org.javassist:javassist:3.29.2-GA") - implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1") - implementation("org.mongodb:bson-kotlinx:5.0.0") + implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.1.2") + implementation("org.mongodb:bson-kotlinx:5.1.2") implementation("com.moandjiezana.toml:toml4j:0.7.2") { exclude(group = "com.google.code.gson", module = "gson") } @@ -76,7 +74,6 @@ dependencies { tasks { shadowJar { minimize { - exclude(dependency("org.litote.kmongo:kmongo-coroutine:.*")) exclude(dependency("org.jetbrains.exposed:.*:.*")) exclude(dependency("com.willfp:ModelEngineBridge:.*")) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt index 037a61636..0723a572b 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt @@ -13,7 +13,10 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction @@ -37,7 +40,7 @@ class LegacyMySQLPersistentDataHandler( private val database = Database.connect(dataSource) private val table = object : UUIDTable("eco_data") { - val data = text("json_data") + val data = text("json_data", eagerLoading = true) } init { @@ -65,7 +68,8 @@ class LegacyMySQLPersistentDataHandler( private inner class LegacySerializer : DataTypeSerializer() { override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { val json = transaction(database) { - table.select { table.id eq uuid } + table.selectAll() + .where { table.id eq uuid } .limit(1) .singleOrNull() ?.get(table.data) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt index d78894b84..bc93f345e 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt @@ -1,5 +1,6 @@ package com.willfp.eco.internal.spigot.data.handlers.impl +import com.mongodb.MongoClientSettings import com.mongodb.client.model.Filters import com.mongodb.client.model.ReplaceOptions import com.mongodb.kotlin.client.coroutine.MongoClient @@ -9,95 +10,184 @@ import com.willfp.eco.core.data.handlers.DataTypeSerializer import com.willfp.eco.core.data.handlers.PersistentDataHandler import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType -import com.willfp.eco.internal.spigot.EcoSpigotPlugin import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import org.bson.BsonArray +import org.bson.BsonBoolean +import org.bson.BsonDecimal128 +import org.bson.BsonDocument +import org.bson.BsonDouble +import org.bson.BsonInt32 +import org.bson.BsonObjectId +import org.bson.BsonString +import org.bson.BsonValue +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.pojo.PojoCodecProvider +import org.bson.types.Decimal128 import java.math.BigDecimal import java.util.UUID class MongoPersistentDataHandler( config: Config ) : PersistentDataHandler("mongo") { + private val codecRegistry = CodecRegistries.fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), + CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build()) + ) + private val client = MongoClient.create(config.getString("url")) private val database = client.getDatabase(config.getString("database")) // Collection name is set for backwards compatibility - private val collection = database.getCollection("uuidprofile") + private val collection = database.getCollection(config.getString("collection")) + .withCodecRegistry(codecRegistry) init { - PersistentDataKeyType.STRING.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.BOOLEAN.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.INT.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.DOUBLE.registerSerializer(this, MongoSerializer()) - PersistentDataKeyType.STRING_LIST.registerSerializer(this, MongoSerializer>()) - PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, MongoSerializer()) + PersistentDataKeyType.STRING.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: String): BsonValue { + return BsonString(value) + } + + override fun deserialize(value: BsonValue): String { + return value.asString().value + } + }) + + PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: Boolean): BsonValue { + return BsonBoolean(value) + } + + override fun deserialize(value: BsonValue): Boolean { + return value.asBoolean().value + } + }) + + PersistentDataKeyType.INT.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: Int): BsonValue { + return BsonInt32(value) + } + + override fun deserialize(value: BsonValue): Int { + return value.asInt32().value + } + }) + + PersistentDataKeyType.DOUBLE.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: Double): BsonValue { + return BsonDouble(value) + } + + override fun deserialize(value: BsonValue): Double { + return value.asDouble().value + } + }) + + PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MongoSerializer>() { + override fun serialize(value: List): BsonValue { + return BsonArray(value.map { BsonString(it) }) + } + + override fun deserialize(value: BsonValue): List { + return value.asArray().values.map { it.asString().value } + } + }) + + PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : MongoSerializer() { + override fun serialize(value: BigDecimal): BsonValue { + return BsonDecimal128(Decimal128(value)) + } + + override fun deserialize(value: BsonValue): BigDecimal { + return value.asDecimal128().value.bigDecimalValue() + } + }) PersistentDataKeyType.CONFIG.registerSerializer(this, object : MongoSerializer() { - override fun convertToMongo(value: Config): Any { - return value.toMap() + private fun deserializeConfigValue(value: BsonValue): Any { + return when (value) { + is BsonString -> value.value + is BsonInt32 -> value.value + is BsonDouble -> value.value + is BsonBoolean -> value.value + is BsonDecimal128 -> value.value.bigDecimalValue() + is BsonArray -> value.values.map { deserializeConfigValue(it) } + is BsonDocument -> value.mapValues { (_, v) -> deserializeConfigValue(v) } + + else -> throw IllegalArgumentException("Could not deserialize config value type ${value::class.simpleName}") + } + } + + private fun serializeConfigValue(value: Any): BsonValue { + return when (value) { + is String -> BsonString(value) + is Int -> BsonInt32(value) + is Double -> BsonDouble(value) + is Boolean -> BsonBoolean(value) + is BigDecimal -> BsonDecimal128(Decimal128(value)) + is List<*> -> BsonArray(value.map { serializeConfigValue(it!!) }) + is Map<*, *> -> BsonDocument().apply { + value.forEach { (k, v) -> append(k.toString(), serializeConfigValue(v!!)) } + } + + else -> throw IllegalArgumentException("Could not serialize config value type ${value::class.simpleName}") + } } - @Suppress("UNCHECKED_CAST") - override fun convertFromMongo(value: Any): Config { - return Configs.fromMap(value as Map) + override fun serialize(value: Config): BsonValue { + return serializeConfigValue(value.toMap()) + } + + override fun deserialize(value: BsonValue): Config { + @Suppress("UNCHECKED_CAST") + return Configs.fromMap(deserializeConfigValue(value.asDocument()) as Map) } }) } override fun getSavedUUIDs(): Set { return runBlocking { - collection.find().toList().map { UUID.fromString(it.uuid) }.toSet() + collection.find().toList().map { + UUID.fromString(it.getString("uuid").value) + }.toSet() } } - private open inner class MongoSerializer : DataTypeSerializer() { + private abstract inner class MongoSerializer : DataTypeSerializer() { override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { return runBlocking { - val profile = collection.find(Filters.eq("uuid", uuid.toString())).firstOrNull() - ?: return@runBlocking null + val filter = Filters.eq("uuid", uuid.toString()) - val value = profile.data[key.key.toString()] - ?: return@runBlocking null + val profile = collection.find(filter) + .firstOrNull() ?: return@runBlocking null - convertFromMongo(value) - } - } + val value = profile[key.key.toString()] ?: return@runBlocking null - protected open fun convertToMongo(value: T): Any { - return value + deserialize(value) + } } override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { runBlocking { - val profile = collection.find(Filters.eq("uuid", uuid.toString())).firstOrNull() - ?: UUIDProfile(uuid.toString(), mutableMapOf()) + val filter = Filters.eq("uuid", uuid.toString()) - profile.data[key.key.toString()] = convertToMongo(value) + val profile = collection.find(filter).firstOrNull() + ?: BsonDocument() + .append("_id", BsonObjectId()) + .append("uuid", BsonString(uuid.toString())) + + profile.append(key.key.toString(), serialize(value)) collection.replaceOne( - Filters.eq("uuid", uuid.toString()), + filter, profile, ReplaceOptions().upsert(true) ) } } - protected open fun convertFromMongo(value: Any): T { - @Suppress("UNCHECKED_CAST") - return value as T - } + protected abstract fun serialize(value: T): BsonValue + protected abstract fun deserialize(value: BsonValue): T } - - @Serializable - private data class UUIDProfile( - // Storing UUID as strings for serialization - @SerialName("_id") val uuid: String, - - // Storing NamespacedKeys as strings for serialization - val data: MutableMap - ) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index 13dd0930a..680360eea 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -12,14 +12,21 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.replace import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update +import org.jetbrains.exposed.sql.upsert import java.math.BigDecimal import java.util.UUID @@ -44,7 +51,7 @@ class MySQLPersistentDataHandler( init { PersistentDataKeyType.STRING.registerSerializer(this, object : DirectStoreSerializer() { override val table = object : KeyTable("string") { - override val value = varchar("value", 128) + override val value = varchar("value", 256) } }.createTable()) @@ -94,7 +101,7 @@ class MySQLPersistentDataHandler( PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MultiValueSerializer() { override val table = object : ListKeyTable("string_list") { - override val value = varchar("value", 128) + override val value = varchar("value", 256) } }.createTable()) } @@ -138,7 +145,8 @@ class MySQLPersistentDataHandler( override fun readAsync(uuid: UUID, key: PersistentDataKey): T? { val stored = transaction(database) { - table.select { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + table.selectAll() + .where { (table.uuid eq uuid) and (table.key eq key.key.toString()) } .limit(1) .singleOrNull() ?.get(table.value) @@ -150,9 +158,7 @@ class MySQLPersistentDataHandler( override fun writeAsync(uuid: UUID, key: PersistentDataKey, value: T) { withRetries { transaction(database) { - table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } - - table.insert { + table.upsert { it[table.uuid] = uuid it[table.key] = key.key.toString() it[table.value] = convertToStored(value) @@ -177,7 +183,8 @@ class MySQLPersistentDataHandler( override fun readAsync(uuid: UUID, key: PersistentDataKey>): List? { val stored = transaction(database) { - table.select { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + table.selectAll() + .where { (table.uuid eq uuid) and (table.key eq key.key.toString()) } .orderBy(table.index) .map { it[table.value] } } @@ -190,8 +197,10 @@ class MySQLPersistentDataHandler( transaction(database) { table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + // Can't get batch inserts to work, would like to fix value.forEachIndexed { index, t -> - table.insert { + // Using replace instead of insert to avoid any deadlock issues + table.replace { it[table.uuid] = uuid it[table.key] = key.key.toString() it[table.index] = index diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt index fb5b9903e..8ac795ac5 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileHandler.kt @@ -21,8 +21,8 @@ class ProfileHandler( private val handlerId = plugin.configYml.getString("data-handler") val localHandler = YamlPersistentDataHandler(plugin) - val defaultHandler = PersistentDataHandlers[handlerId] - ?.create(plugin) ?: throw IllegalArgumentException("Invalid data handler ($handlerId)") + val defaultHandler = PersistentDataHandlers[handlerId]?.create(plugin) + ?: throw IllegalArgumentException("Invalid data handler ($handlerId)") val profileWriter = ProfileWriter(plugin, this) diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index 4a6640f54..8c422be2a 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -16,8 +16,12 @@ perform-data-migration: true mongodb: # The full MongoDB connection URL. url: "" + # The name of the database to use. - database: "eco" + database: eco + + # The collection to use for player data. + collection: profiles mysql: # The table prefix to use for all tables. From 2ccdbe4bc2b0b10d92281ac2ac32f6924259d731 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sun, 25 Aug 2024 16:58:47 +0100 Subject: [PATCH 06/10] Removed bson-kotlinx --- eco-core/core-plugin/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/eco-core/core-plugin/build.gradle.kts b/eco-core/core-plugin/build.gradle.kts index 7cbb676b9..8d3f5b5a4 100644 --- a/eco-core/core-plugin/build.gradle.kts +++ b/eco-core/core-plugin/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation("net.kyori:adventure-platform-bukkit:4.1.0") implementation("org.javassist:javassist:3.29.2-GA") implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.1.2") - implementation("org.mongodb:bson-kotlinx:5.1.2") implementation("com.moandjiezana.toml:toml4j:0.7.2") { exclude(group = "com.google.code.gson", module = "gson") } From 1338c0fadc028c939be7bbc579ca30e7e99a7a23 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sun, 25 Aug 2024 17:05:15 +0100 Subject: [PATCH 07/10] Small cleanup --- .../data/handlers/impl/LegacyMySQLPersistentDataHandler.kt | 4 ---- .../spigot/data/handlers/impl/MongoPersistentDataHandler.kt | 1 - .../spigot/data/handlers/impl/MySQLPersistentDataHandler.kt | 6 ------ .../eco/internal/spigot/data/profiles/ProfileWriter.kt | 1 - 4 files changed, 12 deletions(-) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt index 0723a572b..633d38352 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/LegacyMySQLPersistentDataHandler.kt @@ -13,11 +13,7 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.SqlExpressionBuilder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import java.math.BigDecimal diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt index bc93f345e..f6729e807 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MongoPersistentDataHandler.kt @@ -39,7 +39,6 @@ class MongoPersistentDataHandler( private val client = MongoClient.create(config.getString("url")) private val database = client.getDatabase(config.getString("database")) - // Collection name is set for backwards compatibility private val collection = database.getCollection(config.getString("collection")) .withCodecRegistry(codecRegistry) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index 680360eea..eea96807a 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -12,20 +12,14 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.SqlExpressionBuilder import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteWhere -import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.replace -import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.upsert import java.math.BigDecimal import java.util.UUID diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt index e8f7dfce0..a174878e6 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/profiles/ProfileWriter.kt @@ -2,7 +2,6 @@ package com.willfp.eco.internal.spigot.data.profiles import com.willfp.eco.core.EcoPlugin import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.internal.spigot.data.profiles.impl.localServerIDKey import java.util.UUID import java.util.concurrent.ConcurrentHashMap From c2935a45dcd556a8de6084ce2e205e337a3b79ef Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sun, 25 Aug 2024 17:09:58 +0100 Subject: [PATCH 08/10] Tweaked MySQL retries --- .../data/handlers/impl/MySQLPersistentDataHandler.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index eea96807a..f61dd4abc 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -10,6 +10,8 @@ import com.willfp.eco.core.data.keys.PersistentDataKey import com.willfp.eco.core.data.keys.PersistentDataKeyType import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils @@ -239,10 +241,14 @@ class MySQLPersistentDataHandler( try { return action() } catch (e: Exception) { - if (retries >= 3) { + if (retries >= 5) { throw e } retries++ + + runBlocking { + delay(10) + } } } } From 1a816b0f14ec83eac904983c4aec2ee1fd95f031 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sun, 25 Aug 2024 17:20:15 +0100 Subject: [PATCH 09/10] Fixed list write deadlock for mysql --- .../data/handlers/impl/MySQLPersistentDataHandler.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index f61dd4abc..9a7425230 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -16,6 +16,7 @@ import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere @@ -191,11 +192,15 @@ class MySQLPersistentDataHandler( override fun writeAsync(uuid: UUID, key: PersistentDataKey>, value: List) { withRetries { transaction(database) { - table.deleteWhere { (table.uuid eq uuid) and (table.key eq key.key.toString()) } + // Remove existing values greater than the new list size + table.deleteWhere { + (table.uuid eq uuid) and + (table.key eq key.key.toString()) and + (table.index greaterEq value.size) + } - // Can't get batch inserts to work, would like to fix + // Replace existing values in bounds value.forEachIndexed { index, t -> - // Using replace instead of insert to avoid any deadlock issues table.replace { it[table.uuid] = uuid it[table.key] = key.key.toString() From 413ae4e94df996f6b0acc6e49671f240a877f53f Mon Sep 17 00:00:00 2001 From: Auxilor Date: Sun, 25 Aug 2024 17:29:40 +0100 Subject: [PATCH 10/10] Persistent data tweaks and improvements --- .../eco/core/data/keys/PersistentDataKey.java | 17 +++----- .../eco/internal/spigot/data/KeyRegistry.kt | 39 +------------------ .../impl/MySQLPersistentDataHandler.kt | 8 ++-- .../core-plugin/src/main/resources/config.yml | 4 +- 4 files changed, 15 insertions(+), 53 deletions(-) diff --git a/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKey.java b/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKey.java index 13f80cacf..3b6dbaebc 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKey.java +++ b/eco-api/src/main/java/com/willfp/eco/core/data/keys/PersistentDataKey.java @@ -40,18 +40,11 @@ public final class PersistentDataKey { * @param key The key. * @param type The data type. * @param defaultValue The default value. - * @param isLocal If the key uses local storage. */ public PersistentDataKey(@NotNull final NamespacedKey key, @NotNull final PersistentDataKeyType type, - @NotNull final T defaultValue, - final boolean isLocal) { - this.key = key; - this.defaultValue = defaultValue; - this.type = type; - this.isLocal = isLocal; - - Eco.get().registerPersistentKey(this); + @NotNull final T defaultValue) { + this(key, type, defaultValue, false); } /** @@ -60,14 +53,16 @@ public PersistentDataKey(@NotNull final NamespacedKey key, * @param key The key. * @param type The data type. * @param defaultValue The default value. + * @param isLocal If the key uses local storage. */ public PersistentDataKey(@NotNull final NamespacedKey key, @NotNull final PersistentDataKeyType type, - @NotNull final T defaultValue) { + @NotNull final T defaultValue, + final boolean isLocal) { this.key = key; this.defaultValue = defaultValue; this.type = type; - this.isLocal = false; + this.isLocal = isLocal; Eco.get().registerPersistentKey(this); } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt index 129e22fde..2247a62a8 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/KeyRegistry.kt @@ -1,55 +1,20 @@ package com.willfp.eco.internal.spigot.data -import com.willfp.eco.core.config.interfaces.Config import com.willfp.eco.core.data.keys.PersistentDataKey -import com.willfp.eco.core.data.keys.PersistentDataKeyType import org.bukkit.NamespacedKey -import java.math.BigDecimal object KeyRegistry { private val registry = mutableMapOf>() fun registerKey(key: PersistentDataKey<*>) { - if (this.registry.containsKey(key.key)) { - this.registry.remove(key.key) + if (key.defaultValue == null) { + throw IllegalArgumentException("Default value cannot be null!") } - validateKey(key) - this.registry[key.key] = key } fun getRegisteredKeys(): Set> { return registry.values.toSet() } - - private fun validateKey(key: PersistentDataKey) { - val default = key.defaultValue - - when (key.type) { - PersistentDataKeyType.INT -> if (default !is Int) { - throw IllegalArgumentException("Invalid Data Type! Should be Int") - } - PersistentDataKeyType.DOUBLE -> if (default !is Double) { - throw IllegalArgumentException("Invalid Data Type! Should be Double") - } - PersistentDataKeyType.BOOLEAN -> if (default !is Boolean) { - throw IllegalArgumentException("Invalid Data Type! Should be Boolean") - } - PersistentDataKeyType.STRING -> if (default !is String) { - throw IllegalArgumentException("Invalid Data Type! Should be String") - } - PersistentDataKeyType.STRING_LIST -> if (default !is List<*> || default.firstOrNull() !is String?) { - throw IllegalArgumentException("Invalid Data Type! Should be String List") - } - PersistentDataKeyType.CONFIG -> if (default !is Config) { - throw IllegalArgumentException("Invalid Data Type! Should be Config") - } - PersistentDataKeyType.BIG_DECIMAL -> if (default !is BigDecimal) { - throw IllegalArgumentException("Invalid Data Type! Should be BigDecimal") - } - - else -> throw NullPointerException("Null value found!") - } - } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt index 9a7425230..9a7c88651 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/data/handlers/impl/MySQLPersistentDataHandler.kt @@ -26,6 +26,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.upsert import java.math.BigDecimal import java.util.UUID +import kotlin.math.pow class MySQLPersistentDataHandler( config: Config @@ -241,18 +242,19 @@ class MySQLPersistentDataHandler( } private inline fun withRetries(action: () -> T): T { - var retries = 0 + var retries = 1 while (true) { try { return action() } catch (e: Exception) { - if (retries >= 5) { + if (retries > 5) { throw e } retries++ + // Exponential backoff runBlocking { - delay(10) + delay(2.0.pow(retries.toDouble()).toLong()) } } } diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index 8c422be2a..80b9a619a 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -7,7 +7,7 @@ # How player/server data is saved: # yaml - Stored in data.yml: Good option for single-node servers (i.e. no BungeeCord/Velocity) # mysql - Standard database, great option for multi-node servers (i.e. BungeeCord/Velocity) -# mongo - Alternative database, may suit some servers better +# mongo - Alternative database, great option for multi-node servers (i.e. BungeeCord/Velocity) data-handler: yaml # If data should be migrated automatically when changing data handler. @@ -35,7 +35,7 @@ mysql: port: 3306 database: database user: username - password: passy + password: p4ssw0rd # How many ticks to wait between committing data to a database. This doesn't # affect yaml storage, only MySQL and MongoDB. By default, data is committed