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

@ -43,7 +43,7 @@ public class BukkitSerializer {
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} 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;
}
} 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 Base64Coder.encodeLines(byteOutputStream.toByteArray());
} 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;
}
} 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.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.database.Database;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
@ -23,6 +24,8 @@ public interface HuskSync {
@NotNull RedisManager getRedisManager();
@NotNull DataAdapter getDataAdapter();
@NotNull Settings getSettings();
@NotNull Locales getLocales();

@ -7,11 +7,15 @@ import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.util.UpdateChecker;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload"};
public HuskSyncCommand(@NotNull HuskSync implementor) {
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
}
@ -32,8 +36,8 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
if (updateChecker.isUpdateAvailable(latestVersion)) {
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) [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) [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)"));
} else {
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
public void onConsoleExecute(@NotNull String[] args) {
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;
}
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(
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
case "reload" -> {
@ -71,13 +76,15 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
//todo - MPDB migrator
}
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
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) {

@ -141,6 +141,7 @@ public class Settings {
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_COMPRESS_DATA("synchronization.compress_data", 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_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),

@ -23,11 +23,12 @@ public class AdvancementData {
@SerializedName("completed_criteria")
public Map<String, Date> completedCriteria;
public AdvancementData() {
}
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
this.key = key;
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;
}
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")
public float pitch;
public LocationData() {
}
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
@NotNull String worldEnvironment,
double x, double y, double z,
@ -69,4 +66,8 @@ public class LocationData {
this.yaw = yaw;
this.pitch = pitch;
}
@SuppressWarnings("unused")
protected LocationData() {
}
}

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

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

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

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

@ -1,7 +1,5 @@
package net.william278.husksync.data;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
@ -74,6 +72,8 @@ public class UserData {
this.persistentDataContainerData = persistentDataContainerData;
}
// Empty constructor to facilitate json serialization
@SuppressWarnings("unused")
protected UserData() {
}
@ -109,14 +109,4 @@ public class UserData {
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;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.User;
@ -36,6 +37,20 @@ public abstract class Database {
*/
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
*/
@ -56,11 +71,12 @@ public abstract class Database {
private final ResourceReader resourceReader;
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.dataTableName = dataTableName;
this.maxUserDataRecords = maxUserDataRecords;
this.resourceReader = resourceReader;
this.dataAdapter = dataAdapter;
this.logger = logger;
}

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

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

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

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

Loading…
Cancel
Save