From 12e223618dd1eb45798c6d17dd974e393359d064 Mon Sep 17 00:00:00 2001
From: William
Date: Sun, 11 Feb 2024 15:37:03 +0000
Subject: [PATCH] refactor: data save event order processing, use new method in
DataSyncer (#243)
* fix: fire DataSaveEvent before disconnect
* fix: revert rename `addSnapshot`
* docs: mention `addSnapshot` firing the API event
* refactor: use DataSyncer method for event saving, close #242
* fix: trailing semicolon
---
.../william278/husksync/api/HuskSyncAPI.java | 29 ++++++++--
.../husksync/command/EnderChestCommand.java | 12 +++--
.../husksync/command/InventoryCommand.java | 12 +++--
.../husksync/command/UserDataCommand.java | 15 ++++--
.../husksync/database/Database.java | 32 +++--------
.../husksync/listener/EventListener.java | 10 ++--
.../william278/husksync/sync/DataSyncer.java | 54 ++++++++++++++++++-
.../husksync/sync/DelayDataSyncer.java | 15 +++---
.../husksync/sync/LockstepDataSyncer.java | 24 +++++----
9 files changed, 136 insertions(+), 67 deletions(-)
diff --git a/common/src/main/java/net/william278/husksync/api/HuskSyncAPI.java b/common/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
index 57481a01..5c7a17a4 100644
--- a/common/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
+++ b/common/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
@@ -31,11 +31,13 @@ import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
/**
* The common implementation of the HuskSync API, containing cross-platform API calls.
@@ -262,13 +264,32 @@ public class HuskSyncAPI {
*
* @param user The user to save the data for
* @param snapshot The snapshot to save
+ * @param callback A callback to run after the data has been saved (if the DataSaveEvent was not cancelled)
+ * @apiNote This will fire the {@link net.william278.husksync.event.DataSaveEvent} event, unless
+ * the save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}
+ * @since 3.3.2
+ */
+ public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
+ @Nullable BiConsumer callback) {
+ plugin.runAsync(() -> plugin.getDataSyncer().saveData(
+ user,
+ snapshot instanceof DataSnapshot.Unpacked unpacked
+ ? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
+ callback
+ ));
+ }
+
+ /**
+ * Adds a data snapshot to the database
+ *
+ * @param user The user to save the data for
+ * @param snapshot The snapshot to save
+ * @apiNote This will fire the {@link net.william278.husksync.event.DataSaveEvent} event, unless
+ * * the save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}
* @since 3.0
*/
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
- plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
- user, snapshot instanceof DataSnapshot.Unpacked unpacked
- ? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
- ));
+ this.addSnapshot(user, snapshot, null);
}
/**
diff --git a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java
index 62a7024d..6d81ae3a 100644
--- a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java
@@ -72,8 +72,8 @@ public class EnderChestCommand extends ItemsCommand {
// Creates a new snapshot with the updated enderChest
@SuppressWarnings("DuplicatedCode")
- private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
- final Optional latestData = plugin.getDatabase().getLatestSnapshot(user);
+ private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
+ final Optional latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
@@ -90,10 +90,12 @@ public class EnderChestCommand extends ItemsCommand {
);
});
+ // Save data
final RedisManager redis = plugin.getRedisManager();
- plugin.getDatabase().addSnapshot(user, snapshot);
- redis.sendUserDataUpdate(user, snapshot);
- redis.getUserData(user).ifPresent(data -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
+ plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
+ redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
+ redis.sendUserDataUpdate(user, data);
+ });
}
}
diff --git a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java
index b5b1918e..1523d3d7 100644
--- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java
@@ -72,8 +72,8 @@ public class InventoryCommand extends ItemsCommand {
// Creates a new snapshot with the updated inventory
@SuppressWarnings("DuplicatedCode")
- private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
- final Optional latestData = plugin.getDatabase().getLatestSnapshot(user);
+ private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
+ final Optional latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
@@ -90,10 +90,12 @@ public class InventoryCommand extends ItemsCommand {
);
});
+ // Save data
final RedisManager redis = plugin.getRedisManager();
- plugin.getDatabase().addSnapshot(user, snapshot);
- redis.sendUserDataUpdate(user, snapshot);
- redis.getUserData(user).ifPresent(data -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
+ plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
+ redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
+ redis.sendUserDataUpdate(user, data);
+ });
}
}
diff --git a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java
index fa6b8644..7b06f06a 100644
--- a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java
@@ -21,6 +21,8 @@ package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
+import net.william278.husksync.redis.RedisKeyType;
+import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.DataDumper;
@@ -152,11 +154,14 @@ public class UserDataCommand extends Command implements TabProvider {
);
}));
- // Set the user's data and send a message
- plugin.getDatabase().addSnapshot(user, data);
- plugin.getRedisManager().sendUserDataUpdate(user, data);
- plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(),
- data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage);
+ // Save data
+ final RedisManager redis = plugin.getRedisManager();
+ plugin.getDataSyncer().saveData(user, data, (u, s) -> {
+ redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
+ redis.sendUserDataUpdate(u, s);
+ plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
+ s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
+ });
}
case "pin" -> {
diff --git a/common/src/main/java/net/william278/husksync/database/Database.java b/common/src/main/java/net/william278/husksync/database/Database.java
index 81d432b6..e2b8750e 100644
--- a/common/src/main/java/net/william278/husksync/database/Database.java
+++ b/common/src/main/java/net/william278/husksync/database/Database.java
@@ -23,8 +23,6 @@ import lombok.Getter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
-import net.william278.husksync.data.DataSnapshot.SaveCause;
-import net.william278.husksync.data.UserDataHolder;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
@@ -33,6 +31,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.*;
+import java.util.function.BiConsumer;
/**
* An abstract representation of the plugin database, storing player data.
@@ -156,42 +155,23 @@ public abstract class Database {
@Blocking
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid);
- /**
- * Save user data to the database
- *
- * This will remove the oldest data for the user if the amount of data exceeds the limit as configured
- *
- * @param user The user to add data for
- * @param snapshot The {@link DataSnapshot} to set.
- * The implementation should version it with a random UUID and the current timestamp during insertion.
- * @see UserDataHolder#createSnapshot(SaveCause)
- */
- @Blocking
- public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
- if (snapshot.getSaveCause() != SaveCause.SERVER_SHUTDOWN) {
- plugin.fireEvent(
- plugin.getDataSaveEvent(user, snapshot),
- (event) -> this.addAndRotateSnapshot(user, snapshot)
- );
- return;
- }
-
- this.addAndRotateSnapshot(user, snapshot);
- }
/**
- * Internal - Save user data to the database. This will:
+ * Save user data to the database, doing the following (in order):
*
* - Delete their most recent snapshot, if it was created before the backup frequency time
* - Create the snapshot
* - Rotate snapshot backups
*
+ * This is an expensive blocking method and should be run off the main thread.
*
* @param user The user to add data for
* @param snapshot The {@link DataSnapshot} to set.
+ * @apiNote Prefer {@link net.william278.husksync.sync.DataSyncer#saveData(User, DataSnapshot.Packed, BiConsumer)}.
+ * This method will not fire the {@link net.william278.husksync.event.DataSaveEvent}
*/
@Blocking
- private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
+ public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
final int backupFrequency = plugin.getSettings().getSynchronization().getSnapshotBackupFrequency();
if (!snapshot.isPinned() && backupFrequency > 0) {
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));
diff --git a/common/src/main/java/net/william278/husksync/listener/EventListener.java b/common/src/main/java/net/william278/husksync/listener/EventListener.java
index a5eae805..96522fbe 100644
--- a/common/src/main/java/net/william278/husksync/listener/EventListener.java
+++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java
@@ -81,8 +81,8 @@ public abstract class EventListener {
}
usersInWorld.stream()
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
- .forEach(user -> plugin.getDatabase().addSnapshot(
- user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
+ .forEach(user -> plugin.getDataSyncer().saveData(
+ user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE), null
));
}
@@ -101,7 +101,7 @@ public abstract class EventListener {
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
- plugin.getDatabase().addSnapshot(user, snapshot);
+ plugin.getDataSyncer().saveData(user, snapshot, (u, d) -> plugin.getRedisManager().sendUserDataUpdate(u, d));
}
/**
@@ -123,7 +123,9 @@ public abstract class EventListener {
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
.forEach(user -> {
plugin.lockPlayer(user.getUuid());
- plugin.getDatabase().addSnapshot(user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN));
+ plugin.getDataSyncer().saveData(
+ user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN), null
+ );
});
// Close outstanding connections
diff --git a/common/src/main/java/net/william278/husksync/sync/DataSyncer.java b/common/src/main/java/net/william278/husksync/sync/DataSyncer.java
index e3763e48..bf6747c6 100644
--- a/common/src/main/java/net/william278/husksync/sync/DataSyncer.java
+++ b/common/src/main/java/net/william278/husksync/sync/DataSyncer.java
@@ -22,14 +22,20 @@ package net.william278.husksync.sync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.api.HuskSyncAPI;
import net.william278.husksync.data.DataSnapshot;
+import net.william278.husksync.database.Database;
+import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
+import net.william278.husksync.user.User;
import net.william278.husksync.util.Task;
import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -87,6 +93,42 @@ public abstract class DataSyncer {
*/
public abstract void saveUserData(@NotNull OnlineUser user);
+ /**
+ * Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
+ * first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
+ *
+ * @param user the user to save the data for
+ * @param data the data to save
+ * @param after a consumer to run after data has been saved. Will be run async (off the main thread).
+ * @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is cancelled.
+ * Note that this method can also edit the data before saving it.
+ * @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will not be fired if the
+ * save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}.
+ * @since 3.3.2
+ */
+ @Blocking
+ public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data,
+ @Nullable BiConsumer after) {
+ if (data.getSaveCause() == DataSnapshot.SaveCause.SERVER_SHUTDOWN) {
+ addSnapshotToDatabase(user, data, after);
+ return;
+ }
+ plugin.fireEvent(
+ plugin.getDataSaveEvent(user, data),
+ (event) -> addSnapshotToDatabase(user, data, after)
+ );
+ }
+
+ // Adds a snapshot to the database and runs the after consumer
+ @Blocking
+ private void addSnapshotToDatabase(@NotNull User user, @NotNull DataSnapshot.Packed data,
+ @Nullable BiConsumer after) {
+ getDatabase().addSnapshot(user, data);
+ if (after != null) {
+ after.accept(user, data);
+ }
+ }
+
// Calculates the max attempts the system should listen for user data for based on the latency value
private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + (
@@ -98,7 +140,7 @@ public abstract class DataSyncer {
// Set a user's data from the database, or set them as a new user
@ApiStatus.Internal
protected void setUserFromDatabase(@NotNull OnlineUser user) {
- plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
+ getDatabase().getLatestSnapshot(user).ifPresentOrElse(
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
);
@@ -139,6 +181,16 @@ public abstract class DataSyncer {
task.get().run();
}
+ @NotNull
+ protected RedisManager getRedis() {
+ return plugin.getRedisManager();
+ }
+
+ @NotNull
+ protected Database getDatabase() {
+ return plugin.getDatabase();
+ }
+
/**
* Represents the different available default modes of {@link DataSyncer}
*
diff --git a/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java b/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java
index a00e67c7..a1a90fdb 100644
--- a/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java
+++ b/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java
@@ -39,7 +39,7 @@ public class DelayDataSyncer extends DataSyncer {
plugin.runAsyncDelayed(
() -> {
// Fetch from the database if the user isn't changing servers
- if (!plugin.getRedisManager().getUserServerSwitch(user)) {
+ if (!getRedis().getUserServerSwitch(user)) {
this.setUserFromDatabase(user);
return;
}
@@ -47,7 +47,7 @@ public class DelayDataSyncer extends DataSyncer {
// Listen for the data to be updated
this.listenForRedisData(
user,
- () -> plugin.getRedisManager().getUserData(user).map(data -> {
+ () -> getRedis().getUserData(user).map(data -> {
user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED);
return true;
}).orElse(false)
@@ -58,12 +58,13 @@ public class DelayDataSyncer extends DataSyncer {
}
@Override
- public void saveUserData(@NotNull OnlineUser user) {
+ public void saveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> {
- plugin.getRedisManager().setUserServerSwitch(user);
- final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
- plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_10_SECONDS);
- plugin.getDatabase().addSnapshot(user, data);
+ getRedis().setUserServerSwitch(onlineUser);
+ saveData(
+ onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
+ (user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
+ );
});
}
diff --git a/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java b/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java
index 1b9193ba..85d2b3d4 100644
--- a/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java
+++ b/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java
@@ -33,21 +33,21 @@ public class LockstepDataSyncer extends DataSyncer {
@Override
public void initialize() {
- plugin.getRedisManager().clearUsersCheckedOutOnServer();
+ getRedis().clearUsersCheckedOutOnServer();
}
@Override
public void terminate() {
- plugin.getRedisManager().clearUsersCheckedOutOnServer();
+ getRedis().clearUsersCheckedOutOnServer();
}
// Consume their data when they are checked in
@Override
public void setUserData(@NotNull OnlineUser user) {
this.listenForRedisData(user, () -> {
- if (plugin.getRedisManager().getUserCheckedOut(user).isEmpty()) {
- plugin.getRedisManager().setUserCheckedOut(user, true);
- plugin.getRedisManager().getUserData(user).ifPresentOrElse(
+ if (getRedis().getUserCheckedOut(user).isEmpty()) {
+ getRedis().setUserCheckedOut(user, true);
+ getRedis().getUserData(user).ifPresentOrElse(
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> this.setUserFromDatabase(user)
);
@@ -58,12 +58,16 @@ public class LockstepDataSyncer extends DataSyncer {
}
@Override
- public void saveUserData(@NotNull OnlineUser user) {
+ public void saveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> {
- final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
- plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
- plugin.getRedisManager().setUserCheckedOut(user, false);
- plugin.getDatabase().addSnapshot(user, data);
+ getRedis().setUserServerSwitch(onlineUser);
+ saveData(
+ onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
+ (user, data) -> {
+ getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
+ getRedis().setUserCheckedOut(user, false);
+ }
+ );
});
}