9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2026-01-06 15:41:56 +00:00

User data pinning, version validation checks, fixes

This commit is contained in:
William
2022-07-10 18:12:01 +01:00
parent d1e9f858fe
commit 2e7ed6d9f5
27 changed files with 573 additions and 240 deletions

View File

@@ -3,7 +3,7 @@ package net.william278.husksync.database;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.User;
@@ -157,40 +157,41 @@ public abstract class Database {
* Get the current uniquely versioned user data for a given user, if it exists.
*
* @param user the user to get data for
* @return an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not
* @return an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
*/
public abstract CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull User user);
public abstract CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user);
/**
* Get all {@link VersionedUserData} entries for a user from the database.
* Get all {@link UserDataSnapshot} entries for a user from the database.
*
* @param user The user to get data for
* @return A future returning a list of a user's {@link VersionedUserData} entries
* @return A future returning a list of a user's {@link UserDataSnapshot} entries
*/
public abstract CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user);
public abstract CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user);
/**
* Gets a specific {@link VersionedUserData} entry for a user from the database, by its UUID.
* Gets a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
*
* @param user The user to get data for
* @param versionUuid The UUID of the {@link VersionedUserData} entry to get
* @return A future returning an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to get
* @return A future returning an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
*/
public abstract CompletableFuture<Optional<VersionedUserData>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
public abstract CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
/**
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
*
* @param user The user to prune data for
* @return A future returning void when complete
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
*/
protected abstract CompletableFuture<Void> pruneUserData(@NotNull User user);
protected abstract CompletableFuture<Void> rotateUserData(@NotNull User user);
/**
* Deletes a specific {@link VersionedUserData} entry for a user from the database, by its UUID.
* Deletes a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
*
* @param user The user to get data for
* @param versionUuid The UUID of the {@link VersionedUserData} entry to delete
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to delete
* @return A future returning void when complete
*/
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
@@ -202,10 +203,30 @@ public abstract class Database {
* @param user The user to add data for
* @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion.
* @return A future returning void when complete
* @see VersionedUserData#version(UserData)
* @see UserDataSnapshot#create(UserData)
*/
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
/**
* Pin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
*
* @param user The user to pin the data for
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to pin
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
* @see UserDataSnapshot#pinned()
*/
public abstract CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid);
/**
* Unpin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
*
* @param user The user to unpin the data for
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to unpin
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
* @see UserDataSnapshot#pinned()
*/
public abstract CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid);
/**
* Wipes <b>all</b> {@link UserData} entries from the database.
* <b>This should never be used</b>, except when preparing tables for migration.

View File

@@ -13,8 +13,8 @@ import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.*;
import java.util.Date;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
@@ -206,11 +206,11 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull User user) {
public CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
FROM `%user_data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` DESC
@@ -221,10 +221,11 @@ public class MySqlDatabase extends Database {
final Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
return Optional.of(new VersionedUserData(
return Optional.of(new UserDataSnapshot(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
resultSet.getBoolean("pinned"),
getDataAdapter().fromBytes(dataByteArray)));
}
}
@@ -236,12 +237,12 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user) {
public CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
final List<VersionedUserData> retrievedData = new ArrayList<>();
final List<UserDataSnapshot> retrievedData = new ArrayList<>();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
FROM `%user_data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` DESC;"""))) {
@@ -251,10 +252,11 @@ public class MySqlDatabase extends Database {
final Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
final VersionedUserData data = new VersionedUserData(
final UserDataSnapshot data = new UserDataSnapshot(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
resultSet.getBoolean("pinned"),
getDataAdapter().fromBytes(dataByteArray));
retrievedData.add(data);
}
@@ -268,11 +270,11 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<Optional<VersionedUserData>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
public CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
FROM `%user_data_table%`
WHERE `player_uuid`=? AND `version_uuid`=?
ORDER BY `timestamp` DESC
@@ -284,10 +286,11 @@ public class MySqlDatabase extends Database {
final Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
return Optional.of(new VersionedUserData(
return Optional.of(new UserDataSnapshot(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
resultSet.getBoolean("pinned"),
getDataAdapter().fromBytes(dataByteArray)));
}
}
@@ -299,16 +302,19 @@ public class MySqlDatabase extends Database {
}
@Override
protected CompletableFuture<Void> pruneUserData(@NotNull User user) {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> {
if (data.size() > maxUserDataRecords) {
protected CompletableFuture<Void> rotateUserData(@NotNull User user) {
return CompletableFuture.runAsync(() -> {
final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream()
.filter(dataSnapshot -> !dataSnapshot.pinned()).toList();
if (unpinnedUserData.size() > maxUserDataRecords) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%`
WHERE `player_uuid`=?
AND `pinned` IS FALSE
ORDER BY `timestamp` ASC
LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(data.size() - maxUserDataRecords))))) {
Integer.toString(unpinnedUserData.size() - maxUserDataRecords))))) {
statement.setString(1, user.uuid.toString());
statement.executeUpdate();
}
@@ -316,7 +322,7 @@ public class MySqlDatabase extends Database {
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
}
}
}));
});
}
@Override
@@ -361,7 +367,45 @@ public class MySqlDatabase extends Database {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
}
}
}).thenRun(() -> pruneUserData(user).join());
}).thenRun(() -> rotateUserData(user).join());
}
@Override
public CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid) {
return CompletableFuture.runAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE `%user_data_table%`
SET `pinned`=TRUE
WHERE `player_uuid`=? AND `version_uuid`=?
LIMIT 1;"""))) {
statement.setString(1, user.uuid.toString());
statement.setString(2, versionUuid.toString());
statement.executeUpdate();
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to pin user data in the database", e);
}
});
}
@Override
public CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid) {
return CompletableFuture.runAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE `%user_data_table%`
SET `pinned`=FALSE
WHERE `player_uuid`=? AND `version_uuid`=?
LIMIT 1;"""))) {
statement.setString(1, user.uuid.toString());
statement.setString(2, versionUuid.toString());
statement.executeUpdate();
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to unpin user data in the database", e);
}
});
}
@Override