Implement compression via DataAdapter, add option to disable compression, better exception handling

feat/data-edit-commands
William 3 years ago
parent f2d4bec138
commit 1829526aa7

@ -12,6 +12,9 @@ import net.william278.husksync.command.HuskSyncCommand;
import net.william278.husksync.command.Permission; import net.william278.husksync.command.Permission;
import net.william278.husksync.config.Locales; import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.CompressedDataAdapter;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.JsonDataAdapter;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.BukkitEventListener;
@ -46,6 +49,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
private EventListener eventListener; private EventListener eventListener;
private DataAdapter dataAdapter;
private Settings settings; private Settings settings;
private Locales locales; private Locales locales;
@ -91,9 +96,18 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
} }
return loadedSettings; return loadedSettings;
}).join(); }).join();
}).thenApply(succeeded -> {
if (succeeded) {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) {
dataAdapter = new CompressedDataAdapter();
} else {
dataAdapter = new JsonDataAdapter();
}
}
return succeeded;
}).thenApply(succeeded -> { }).thenApply(succeeded -> {
// Establish connection to the database // Establish connection to the database
this.database = new MySqlDatabase(settings, resourceReader, logger); this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter);
if (succeeded) { if (succeeded) {
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database..."); getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database...");
final CompletableFuture<Boolean> databaseConnectFuture = new CompletableFuture<>(); final CompletableFuture<Boolean> databaseConnectFuture = new CompletableFuture<>();
@ -101,7 +115,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
final boolean initialized = this.database.initialize(); final boolean initialized = this.database.initialize();
if (!initialized) { if (!initialized) {
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. " getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. "
+ "Please check the supplied database credentials in the config file"); + "Please check the supplied database credentials in the config file");
databaseConnectFuture.completeAsync(() -> false); databaseConnectFuture.completeAsync(() -> false);
return; return;
} }
@ -113,13 +127,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return false; return false;
}).thenApply(succeeded -> { }).thenApply(succeeded -> {
// Establish connection to the Redis server // Establish connection to the Redis server
this.redisManager = new RedisManager(settings); this.redisManager = new RedisManager(settings, dataAdapter);
if (succeeded) { if (succeeded) {
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server..."); getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
return this.redisManager.initialize().thenApply(initialized -> { return this.redisManager.initialize().thenApply(initialized -> {
if (!initialized) { if (!initialized) {
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. " getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. "
+ "Please check the supplied Redis credentials in the config file"); + "Please check the supplied Redis credentials in the config file");
return false; return false;
} }
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server"); getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
@ -167,7 +181,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
// Handle failed initialization // Handle failed initialization
if (!succeeded) { if (!succeeded) {
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " +
"The plugin will now be disabled"); "The plugin will now be disabled");
getServer().getPluginManager().disablePlugin(this); getServer().getPluginManager().disablePlugin(this);
} else { } else {
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion()); getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion());
@ -207,6 +221,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return redisManager; return redisManager;
} }
@Override
public @NotNull DataAdapter getDataAdapter() {
return dataAdapter;
}
@Override @Override
public @NotNull Settings getSettings() { public @NotNull Settings getSettings() {
return settings; return settings;

@ -43,7 +43,7 @@ public class BukkitSerializer {
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray()); return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("Failed to serialize item stack data"); throw new DataDeserializationException("Failed to serialize item stack data", e);
} }
}); });
} }
@ -78,7 +78,7 @@ public class BukkitSerializer {
return inventoryContents; return inventoryContents;
} }
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Failed to deserialize item stack data"); throw new DataDeserializationException("Failed to deserialize item stack data", e);
} }
}); });
} }
@ -132,7 +132,7 @@ public class BukkitSerializer {
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray()); return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("Failed to serialize potion effect data"); throw new DataDeserializationException("Failed to serialize potion effect data", e);
} }
}); });
} }
@ -167,7 +167,7 @@ public class BukkitSerializer {
return potionEffects; return potionEffects;
} }
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Failed to deserialize potion effects", e); throw new DataDeserializationException("Failed to deserialize potion effects", e);
} }
}); });
} }

@ -2,6 +2,7 @@ package net.william278.husksync;
import net.william278.husksync.config.Locales; import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
@ -23,6 +24,8 @@ public interface HuskSync {
@NotNull RedisManager getRedisManager(); @NotNull RedisManager getRedisManager();
@NotNull DataAdapter getDataAdapter();
@NotNull Settings getSettings(); @NotNull Settings getSettings();
@NotNull Locales getLocales(); @NotNull Locales getLocales();

@ -7,11 +7,15 @@ import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.util.UpdateChecker; import net.william278.husksync.util.UpdateChecker;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable { public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload"};
public HuskSyncCommand(@NotNull HuskSync implementor) { public HuskSyncCommand(@NotNull HuskSync implementor) {
super("husksync", Permission.COMMAND_HUSKSYNC, implementor); super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
} }
@ -32,8 +36,8 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> { updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
if (updateChecker.isUpdateAvailable(latestVersion)) { if (updateChecker.isUpdateAvailable(latestVersion)) {
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" + player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" +
"[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" + "[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
"[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)")); "[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
} else { } else {
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + latestVersion + "](#00fb9a)")); player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + latestVersion + "](#00fb9a)"));
} }
@ -56,11 +60,12 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
@Override @Override
public void onConsoleExecute(@NotNull String[] args) { public void onConsoleExecute(@NotNull String[] args) {
if (args.length < 1) { if (args.length < 1) {
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: /husksync <update/info/reload/migrate>"); plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/info/reload/migrate>\"");
return; return;
} }
switch (args[0].toLowerCase()) { switch (args[0].toLowerCase()) {
case "update", "version" -> new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole(); case "update", "version" ->
new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole();
case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown( case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown(
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion()))); Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
case "reload" -> { case "reload" -> {
@ -71,13 +76,15 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
//todo - MPDB migrator //todo - MPDB migrator
} }
default -> default ->
plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: /husksync <update/info/reload/migrate>"); plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: \"husksync <update/info/reload/migrate>\"");
} }
} }
@Override @Override
public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) { public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
return null; return Arrays.stream(COMMAND_ARGUMENTS)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
} }
private void displayPluginInformation(@NotNull OnlineUser player) { private void displayPluginInformation(@NotNull OnlineUser player) {

@ -141,6 +141,7 @@ public class Settings {
SYNCHRONIZATION_MAX_USER_DATA_RECORDS("synchronization.max_user_data_records", OptionType.INTEGER, 5), SYNCHRONIZATION_MAX_USER_DATA_RECORDS("synchronization.max_user_data_records", OptionType.INTEGER, 5),
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true), SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true), SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true), SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true), SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),

@ -23,11 +23,12 @@ public class AdvancementData {
@SerializedName("completed_criteria") @SerializedName("completed_criteria")
public Map<String, Date> completedCriteria; public Map<String, Date> completedCriteria;
public AdvancementData() {
}
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) { public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
this.key = key; this.key = key;
this.completedCriteria = awardedCriteria; this.completedCriteria = awardedCriteria;
} }
@SuppressWarnings("unused")
protected AdvancementData() {
}
} }

@ -0,0 +1,27 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import java.io.IOException;
public class CompressedDataAdapter extends JsonDataAdapter {
@Override
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
try {
return Snappy.compress(super.toBytes(data));
} catch (IOException e) {
throw new DataAdaptionException("Failed to compress data", e);
}
}
@Override
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
try {
return super.fromBytes(Snappy.uncompress(data));
} catch (IOException e) {
throw new DataAdaptionException("Failed to decompress data", e);
}
}
}

@ -0,0 +1,29 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
/**
* An adapter that adapts {@link UserData} to and from a portable byte array.
*/
public interface DataAdapter {
/**
* Converts {@link UserData} to a byte array.
*
* @param data The {@link UserData} to adapt.
* @return The byte array.
* @throws DataAdaptionException If an error occurred during adaptation.
*/
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
/**
* Converts a byte array to {@link UserData}.
*
* @param data The byte array to adapt.
* @return The {@link UserData}.
* @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid.
*/
@NotNull
UserData fromBytes(final byte[] data) throws DataAdaptionException;
}

@ -0,0 +1,11 @@
package net.william278.husksync.data;
/**
* Indicates an error occurred during {@link UserData} adaptation to and from (compressed) json.
*/
public class DataAdaptionException extends RuntimeException {
protected DataAdaptionException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,13 @@
package net.william278.husksync.data;
/**
* Indicates an error occurred during base-64 serialization and deserialization of data.
* </p>
* For example, an exception deserializing {@link InventoryData} item stack or {@link PotionEffectData} potion effect arrays
*/
public class DataDeserializationException extends RuntimeException {
protected DataDeserializationException(String message, Throwable cause) {
super(message, cause);
}
}

@ -18,7 +18,8 @@ public class InventoryData {
this.serializedInventory = serializedInventory; this.serializedInventory = serializedInventory;
} }
public InventoryData() { @SuppressWarnings("unused")
protected InventoryData() {
} }
} }

@ -0,0 +1,24 @@
package net.william278.husksync.data;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
public class JsonDataAdapter implements DataAdapter {
@Override
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
return new GsonBuilder().create().toJson(data).getBytes(StandardCharsets.UTF_8);
}
@Override
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
try {
return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class);
} catch (JsonSyntaxException e) {
throw new DataAdaptionException("Failed to parse JSON data", e);
}
}
}

@ -53,9 +53,6 @@ public class LocationData {
@SerializedName("pitch") @SerializedName("pitch")
public float pitch; public float pitch;
public LocationData() {
}
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid, public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
@NotNull String worldEnvironment, @NotNull String worldEnvironment,
double x, double y, double z, double x, double y, double z,
@ -69,4 +66,8 @@ public class LocationData {
this.yaw = yaw; this.yaw = yaw;
this.pitch = pitch; this.pitch = pitch;
} }
@SuppressWarnings("unused")
protected LocationData() {
}
} }

@ -20,7 +20,8 @@ public class PersistentDataContainerData {
this.persistentDataMap = persistentDataMap; this.persistentDataMap = persistentDataMap;
} }
public PersistentDataContainerData() { @SuppressWarnings("unused")
protected PersistentDataContainerData() {
} }
} }

@ -15,7 +15,8 @@ public class PotionEffectData {
this.serializedPotionEffects = serializedInventory; this.serializedPotionEffects = serializedInventory;
} }
public PotionEffectData() { @SuppressWarnings("unused")
protected PotionEffectData() {
} }
} }

@ -45,7 +45,8 @@ public class StatisticsData {
this.entityStatistics = entityStatistics; this.entityStatistics = entityStatistics;
} }
public StatisticsData() { @SuppressWarnings("unused")
protected StatisticsData() {
} }
} }

@ -98,6 +98,7 @@ public class StatusData {
this.isFlying = isFlying; this.isFlying = isFlying;
} }
public StatusData() { @SuppressWarnings("unused")
protected StatusData() {
} }
} }

@ -1,7 +1,5 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -74,6 +72,8 @@ public class UserData {
this.persistentDataContainerData = persistentDataContainerData; this.persistentDataContainerData = persistentDataContainerData;
} }
// Empty constructor to facilitate json serialization
@SuppressWarnings("unused")
protected UserData() { protected UserData() {
} }
@ -109,14 +109,4 @@ public class UserData {
return persistentDataContainerData; return persistentDataContainerData;
} }
@NotNull
public static UserData fromJson(String json) throws JsonSyntaxException {
return new GsonBuilder().create().fromJson(json, UserData.class);
}
@NotNull
public String toJson() {
return new GsonBuilder().create().toJson(this);
}
} }

@ -1,5 +1,6 @@
package net.william278.husksync.database; package net.william278.husksync.database;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@ -36,6 +37,20 @@ public abstract class Database {
*/ */
protected final int maxUserDataRecords; protected final int maxUserDataRecords;
/**
* {@link DataAdapter} implementation used for adapting {@link UserData} to and from JSON
*/
private final DataAdapter dataAdapter;
/**
* Returns the {@link DataAdapter} used to adapt {@link UserData} to and from JSON
*
* @return instance of the {@link DataAdapter} implementation
*/
protected DataAdapter getDataAdapter() {
return dataAdapter;
}
/** /**
* Logger instance used for database error logging * Logger instance used for database error logging
*/ */
@ -56,11 +71,12 @@ public abstract class Database {
private final ResourceReader resourceReader; private final ResourceReader resourceReader;
protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords, protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
@NotNull ResourceReader resourceReader, @NotNull Logger logger) { @NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) {
this.playerTableName = playerTableName; this.playerTableName = playerTableName;
this.dataTableName = dataTableName; this.dataTableName = dataTableName;
this.maxUserDataRecords = maxUserDataRecords; this.maxUserDataRecords = maxUserDataRecords;
this.resourceReader = resourceReader; this.resourceReader = resourceReader;
this.dataAdapter = dataAdapter;
this.logger = logger; this.logger = logger;
} }

@ -2,6 +2,8 @@ package net.william278.husksync.database;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.DataAdaptionException;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@ -52,22 +54,23 @@ public class MySqlDatabase extends Database {
*/ */
private HikariDataSource connectionPool; private HikariDataSource connectionPool;
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger) { public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger,
@NotNull DataAdapter dataAdapter) {
super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME), super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME),
settings.getStringValue(Settings.ConfigOption.DATABASE_DATA_TABLE_NAME), settings.getStringValue(Settings.ConfigOption.DATABASE_DATA_TABLE_NAME),
settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_RECORDS), settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_RECORDS),
resourceReader, logger); resourceReader, dataAdapter, logger);
mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST); this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT); this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME); this.mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME); this.mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD); this.mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS); this.mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS);
hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE); this.hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE);
hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE); this.hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE);
hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME); this.hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME);
hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE); this.hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE);
hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT); this.hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT);
} }
/** /**
@ -219,16 +222,15 @@ public class MySqlDatabase extends Database {
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) { if (resultSet.next()) {
final Blob blob = resultSet.getBlob("data"); final Blob blob = resultSet.getBlob("data");
final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length()); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free(); blob.free();
return Optional.of(new VersionedUserData( return Optional.of(new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")), UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()), Date.from(resultSet.getTimestamp("timestamp").toInstant()),
UserData.fromJson(new String(Snappy.uncompress(compressedDataJson), getDataAdapter().fromBytes(dataByteArray)));
StandardCharsets.UTF_8))));
} }
} }
} catch (SQLException | IOException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
} }
return Optional.empty(); return Optional.empty();
@ -249,18 +251,17 @@ public class MySqlDatabase extends Database {
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) { while (resultSet.next()) {
final Blob blob = resultSet.getBlob("data"); final Blob blob = resultSet.getBlob("data");
final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length()); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free(); blob.free();
final VersionedUserData data = new VersionedUserData( final VersionedUserData data = new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")), UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()), Date.from(resultSet.getTimestamp("timestamp").toInstant()),
UserData.fromJson(new String(Snappy.uncompress(compressedDataJson), getDataAdapter().fromBytes(dataByteArray));
StandardCharsets.UTF_8)));
retrievedData.add(data); retrievedData.add(data);
} }
return retrievedData; return retrievedData;
} }
} catch (SQLException | IOException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
} }
return retrievedData; return retrievedData;
@ -297,11 +298,11 @@ public class MySqlDatabase extends Database {
(`player_uuid`,`version_uuid`,`timestamp`,`data`) (`player_uuid`,`version_uuid`,`timestamp`,`data`)
VALUES (?,UUID(),NOW(),?);"""))) { VALUES (?,UUID(),NOW(),?);"""))) {
statement.setString(1, user.uuid.toString()); statement.setString(1, user.uuid.toString());
statement.setBlob(2, new ByteArrayInputStream(Snappy statement.setBlob(2, new ByteArrayInputStream(
.compress(userData.toJson().getBytes(StandardCharsets.UTF_8)))); getDataAdapter().toBytes(userData)));
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException | IOException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e); getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
} }
}).thenRun(() -> pruneUserDataRecords(user).join()); }).thenRun(() -> pruneUserDataRecords(user).join());

@ -1,9 +1,8 @@
package net.william278.husksync.listener; package net.william278.husksync.listener;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import net.william278.husksync.redis.RedisManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.HashSet; import java.util.HashSet;
@ -94,7 +93,7 @@ public abstract class EventListener {
} }
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) { public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {
if (disabling) { if (disabling || !huskSync.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) {
return; return;
} }
CompletableFuture.runAsync(() -> usersInWorld.forEach(user -> CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->

@ -1,6 +1,7 @@
package net.william278.husksync.redis; package net.william278.husksync.redis;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -26,6 +27,7 @@ public class RedisManager {
private static String clusterId = ""; private static String clusterId = "";
private final JedisPoolConfig jedisPoolConfig; private final JedisPoolConfig jedisPoolConfig;
private final DataAdapter dataAdapter;
private final String redisHost; private final String redisHost;
private final int redisPort; private final int redisPort;
@ -34,8 +36,9 @@ public class RedisManager {
private JedisPool jedisPool; private JedisPool jedisPool;
public RedisManager(@NotNull Settings settings) { public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter) {
clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID); clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID);
this.dataAdapter = dataAdapter;
this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST); this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST);
this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT); this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT);
this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD); this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD);
@ -72,9 +75,8 @@ public class RedisManager {
/** /**
* Set a user's data to the Redis server * Set a user's data to the Redis server
* *
* @param user the user to set data for * @param user the user to set data for
* @param userData the user's data to set * @param userData the user's data to set
* @param redisKeyType the type of key to set the data with. This determines the time to live for the data.
* @return a future returning void when complete * @return a future returning void when complete
*/ */
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) { public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
@ -82,11 +84,9 @@ public class RedisManager {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
// Set the user's data as a compressed byte array of the json using Snappy // Set the user's data as a compressed byte array of the json using Snappy
jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid), RedisKeyType.DATA_UPDATE.timeToLive, jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid),
Snappy.compress(userData.toJson().getBytes(StandardCharsets.UTF_8))); RedisKeyType.DATA_UPDATE.timeToLive,
System.out.println("Set key at " + new Date().getTime()); dataAdapter.toBytes(userData));
} catch (IOException e) {
throw new RuntimeException(e);
} }
}); });
} catch (Exception e) { } catch (Exception e) {
@ -107,8 +107,7 @@ public class RedisManager {
/** /**
* Fetch a user's data from the Redis server and consume the key if found * Fetch a user's data from the Redis server and consume the key if found
* *
* @param user The user to fetch data for * @param user The user to fetch data for
* @param redisKeyType The type of key to fetch
* @return The user's data, if it's present on the database. Otherwise, an empty optional. * @return The user's data, if it's present on the database. Otherwise, an empty optional.
*/ */
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) { public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
@ -116,18 +115,15 @@ public class RedisManager {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid); final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid);
System.out.println("Reading key at " + new Date().getTime()); System.out.println("Reading key at " + new Date().getTime());
final byte[] compressedJson = jedis.get(key); final byte[] dataByteArray = jedis.get(key);
if (compressedJson == null) { if (dataByteArray == null) {
return Optional.empty(); return Optional.empty();
} }
// Consume the key (delete from redis) // Consume the key (delete from redis)
jedis.del(key); jedis.del(key);
// Use Snappy to decompress the json // Use Snappy to decompress the json
return Optional.of(UserData.fromJson(new String(Snappy.uncompress(compressedJson), return Optional.of(dataAdapter.fromBytes(dataByteArray));
StandardCharsets.UTF_8)));
} catch (IOException e) {
throw new RuntimeException(e);
} }
}); });
} }

@ -36,6 +36,7 @@ redis:
synchronization: synchronization:
max_user_data_records: 5 max_user_data_records: 5
save_on_world_save: true save_on_world_save: true
compress_data: true
features: features:
inventories: true inventories: true
ender_chests: true ender_chests: true

Loading…
Cancel
Save