9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-28 02:59:13 +00:00

Fix data sync when changing servers, consume keys when retrieved

This commit is contained in:
William
2022-07-04 00:23:31 +01:00
parent 38c261871a
commit d78dd42b72
8 changed files with 232 additions and 122 deletions

View File

@@ -35,7 +35,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
"[•](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));
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + latestVersion + "](#00fb9a)"));
}
});
}
@@ -46,17 +46,17 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
return;
}
plugin.reload();
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) &#00fb9a&| Reloaded config & message files."));
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Reloaded config & message files.]((#00fb9a)"));
}
default ->
plugin.getLocales().getLocale("error_invalid_syntax", "/husksync <update|info|reload>").ifPresent(player::sendMessage);
plugin.getLocales().getLocale("error_invalid_syntax", "/husksync <update/info/reload>").ifPresent(player::sendMessage);
}
}
@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()) {
@@ -71,7 +71,7 @@ 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>");
}
}

View File

@@ -15,11 +15,11 @@ import java.util.regex.Pattern;
public class Locales {
public static final String PLUGIN_INFORMATION = """
[HuskSync](#00fb9a bold) [| Version %version%(#00fb9a)
[HuskSync](#00fb9a bold) [| Version %version%](#00fb9a)
[A modern, cross-server player data synchronization system](gray)
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
[• Contributors:](white) [HarvelsX](gray show_text=&7Code)
[• Translators:](white) [Namiu/うにたろう](gray show_text=&7Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua)
[• Translators:](white) [Namiu](gray show_text=&7\\(うにたろう\\) - Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua)
[• Plugin Info:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/)
[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
[• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";

View File

@@ -271,19 +271,19 @@ public class MySqlDatabase extends Database {
protected CompletableFuture<Void> pruneUserDataRecords(@NotNull User user) {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> {
if (data.size() > maxUserDataRecords) {
Collections.reverse(data);
data.subList(0, data.size() - maxUserDataRecords).forEach(dataToDelete -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%data_table%`
WHERE `version_uuid`=?"""))) {
statement.setString(1, dataToDelete.versionUUID().toString());
statement.executeUpdate();
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` ASC
LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(data.size() - maxUserDataRecords))))) {
statement.setString(1, user.uuid.toString());
statement.executeUpdate();
}
});
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
}
}
}));
}
@@ -306,7 +306,7 @@ public class MySqlDatabase extends Database {
} catch (SQLException | IOException e) {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
}
})/*.thenRunAsync(() -> pruneUserDataRecords(user).join())*/;
}).thenRun(() -> pruneUserDataRecords(user).join());
}
@Override

View File

@@ -2,6 +2,7 @@ package net.william278.husksync.listener;
import net.william278.husksync.HuskSync;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import net.william278.husksync.redis.RedisManager;
import org.jetbrains.annotations.NotNull;
@@ -9,8 +10,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class EventListener {
public abstract class EventListener {
/**
* The plugin instance
@@ -34,30 +39,58 @@ public class EventListener {
}
public final void handlePlayerJoin(@NotNull OnlineUser user) {
if (user.isDead()) {
return;
}
usersAwaitingSync.add(user.uuid);
huskSync.getRedisManager().getUserData(user, RedisManager.RedisKeyType.SERVER_CHANGE).thenAccept(
cachedUserData -> cachedUserData.ifPresentOrElse(
userData -> user.setData(userData, huskSync.getSettings()).join(),
() -> huskSync.getDatabase().getCurrentUserData(user).thenAccept(
databaseUserData -> databaseUserData.ifPresent(
data -> user.setData(data.userData(), huskSync.getSettings()).join())).join())).thenRunAsync(
() -> {
huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
usersAwaitingSync.remove(user.uuid);
huskSync.getDatabase().ensureUser(user).join();
CompletableFuture.runAsync(() -> huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
if (!changingServers) {
// Fetch from the database if the user isn't changing servers
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
} else {
final int TIME_OUT_MILLISECONDS = 3200;
CompletableFuture.runAsync(() -> {
final AtomicInteger currentMilliseconds = new AtomicInteger(0);
final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// Set the user as soon as the source server has set the data to redis
executor.scheduleAtFixedRate(() -> {
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
executor.shutdown();
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
return;
}
huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData ->
redisUserData.ifPresent(redisData -> {
user.setData(redisData, huskSync.getSettings()).join();
executor.shutdown();
})).join();
currentMilliseconds.addAndGet(200);
}, 0, 200L, TimeUnit.MILLISECONDS);
});
}
}));
}
private CompletableFuture<Void> setUserFromDatabase(@NotNull OnlineUser user) {
return huskSync.getDatabase().getCurrentUserData(user)
.thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData -> user
.setData(databaseData.userData(), huskSync.getSettings()).join()));
}
private void handleSynchronisationCompletion(@NotNull OnlineUser user) {
huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
usersAwaitingSync.remove(user.uuid);
huskSync.getDatabase().ensureUser(user).join();
}
public final void handlePlayerQuit(@NotNull OnlineUser user) {
if (disabling) {
return;
}
user.getUserData().thenAccept(userData -> {
System.out.println(userData.userData().toJson());
huskSync.getRedisManager()
.setUserData(user, userData.userData(), RedisManager.RedisKeyType.SERVER_CHANGE).thenRun(
() -> huskSync.getDatabase().setUserData(user, userData).join());
});
huskSync.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept(
userData -> huskSync.getRedisManager().setUserData(user, userData.userData()).thenRun(
() -> huskSync.getDatabase().setUserData(user, userData).join())));
}
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {

View File

@@ -39,7 +39,8 @@ public abstract class OnlineUser extends User {
public abstract CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
final boolean setHealth, final boolean setMaxHealth,
final boolean setHunger, final boolean setExperience,
final boolean setGameMode, boolean setFlying);
final boolean setGameMode, final boolean setFlying,
final boolean setSelectedItemSlot);
/**
* Get the player's inventory {@link InventoryData} contents
@@ -147,6 +148,20 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData);
/**
* Indicates if the player is currently dead
*
* @return {@code true} if the player is dead (health <= 0); {@code false} otherwise
*/
public abstract boolean isDead();
/**
* Indicates if the player has gone offline
*
* @return {@code true} if the player has left the server; {@code false} otherwise
*/
public abstract boolean isOffline();
/**
* Set {@link UserData} to a player
*
@@ -156,32 +171,45 @@ public abstract class OnlineUser extends User {
*/
public final CompletableFuture<Void> setData(@NotNull UserData data, @NotNull Settings settings) {
return CompletableFuture.runAsync(() -> {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
setInventory(data.getInventoryData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
setEnderChest(data.getEnderChestData()).join();
}
setStatus(data.getStatusData(), settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)).join();
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
setPotionEffects(data.getPotionEffectData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
setAdvancements(data.getAdvancementData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
setStatistics(data.getStatisticData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
setPersistentDataContainer(data.getPersistentDataContainerData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
setLocation(data.getLocationData()).join();
try {
// Don't set offline players
if (isOffline()) {
return;
}
// Don't set dead players
if (isDead()) {
return;
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
setInventory(data.getInventoryData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
setEnderChest(data.getEnderChestData()).join();
}
setStatus(data.getStatusData(), settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)).join();
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
setPotionEffects(data.getPotionEffectData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
setAdvancements(data.getAdvancementData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
setStatistics(data.getStatisticData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
setPersistentDataContainer(data.getPersistentDataContainerData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
setLocation(data.getLocationData()).join();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}

View File

@@ -12,6 +12,7 @@ import redis.clients.jedis.exceptions.JedisException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -76,14 +77,14 @@ public class RedisManager {
* @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
*/
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
@NotNull RedisKeyType redisKeyType) {
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
try {
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, user.uuid), redisKeyType.timeToLive,
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);
}
@@ -94,21 +95,34 @@ public class RedisManager {
return null;
}
public CompletableFuture<Void> setUserServerSwitch(@NotNull User user) {
return CompletableFuture.runAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(getKey(RedisKeyType.SERVER_SWITCH, user.uuid),
RedisKeyType.SERVER_SWITCH.timeToLive, new byte[0]);
}
});
}
/**
* Fetch a user's data from the Redis server
* 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
* @return The user's data, if it's present on the database. Otherwise, an empty optional.
*/
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user,
@NotNull RedisKeyType redisKeyType) {
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] compressedJson = jedis.get(getKey(redisKeyType, user.uuid));
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) {
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)));
@@ -118,6 +132,21 @@ public class RedisManager {
});
}
public CompletableFuture<Boolean> getUserServerSwitch(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid);
final byte[] compressedJson = jedis.get(key);
if (compressedJson == null) {
return false;
}
// Consume the key (delete from redis)
jedis.del(key);
return true;
}
});
}
public void close() {
if (jedisPool != null) {
if (!jedisPool.isClosed()) {
@@ -132,7 +161,8 @@ public class RedisManager {
public enum RedisKeyType {
CACHE(60 * 60 * 24),
SERVER_CHANGE(2);
DATA_UPDATE(10),
SERVER_SWITCH(10);
public final int timeToLive;