diff --git a/Plan/common/src/main/java/com/djrapitops/plan/gathering/ShutdownDataPreservation.java b/Plan/common/src/main/java/com/djrapitops/plan/gathering/ShutdownDataPreservation.java index 127e2be29..09ea554c0 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/gathering/ShutdownDataPreservation.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/gathering/ShutdownDataPreservation.java @@ -22,8 +22,7 @@ import com.djrapitops.plan.gathering.domain.FinishedSession; import com.djrapitops.plan.settings.locale.Locale; import com.djrapitops.plan.settings.locale.lang.PluginLang; import com.djrapitops.plan.storage.database.DBSystem; -import com.djrapitops.plan.storage.database.queries.LargeStoreQueries; -import com.djrapitops.plan.storage.database.transactions.Transaction; +import com.djrapitops.plan.storage.database.transactions.events.ShutdownDataPreservationTransaction; import com.djrapitops.plan.storage.file.PlanFiles; import com.djrapitops.plan.utilities.logging.ErrorContext; import com.djrapitops.plan.utilities.logging.ErrorLogger; @@ -87,12 +86,7 @@ public class ShutdownDataPreservation extends TaskSystem.Task { private void storeInDB(List finishedSessions) { if (!finishedSessions.isEmpty()) { try { - dbSystem.getDatabase().executeTransaction(new Transaction() { - @Override - protected void performOperations() { - execute(LargeStoreQueries.storeAllSessionsWithKillAndWorldData(finishedSessions)); - } - }).get(); + dbSystem.getDatabase().executeTransaction(new ShutdownDataPreservationTransaction(finishedSessions)).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/FinishedSession.java b/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/FinishedSession.java index 35a871fe6..6845eee0a 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/FinishedSession.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/FinishedSession.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.gathering.domain; import com.djrapitops.plan.delivery.domain.DateHolder; +import com.djrapitops.plan.delivery.domain.PlayerName; import com.djrapitops.plan.gathering.domain.event.JoinAddress; import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable; @@ -143,6 +144,7 @@ public class FinishedSession implements DateHolder { asOptionals.get(7).ifPresent(value -> extraData.put(MobKillCounter.class, gson.fromJson(value, MobKillCounter.class))); asOptionals.get(8).ifPresent(value -> extraData.put(DeathCounter.class, gson.fromJson(value, DeathCounter.class))); asOptionals.get(9).ifPresent(value -> extraData.put(JoinAddress.class, new JoinAddress(value))); + asOptionals.get(10).ifPresent(value -> extraData.put(PlayerName.class, new PlayerName(value))); return Optional.of(new FinishedSession(playerUUID, serverUUID, start, end, afkTime, extraData)); } @@ -194,7 +196,8 @@ public class FinishedSession implements DateHolder { getExtraData(PlayerKills.class).orElseGet(PlayerKills::new).toJson() + ';' + getExtraData(MobKillCounter.class).orElseGet(MobKillCounter::new).toJson() + ';' + getExtraData(DeathCounter.class).orElseGet(DeathCounter::new).toJson() + ';' + - getExtraData(JoinAddress.class).orElseGet(() -> new JoinAddress(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP)).getAddress(); + getExtraData(JoinAddress.class).map(JoinAddress::getAddress).orElse(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP) + ';' + + getExtraData(PlayerName.class).map(PlayerName::get).orElseGet(playerUUID::toString); } public static class Id { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/BaseUserQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/BaseUserQueries.java index ea3011511..71df9ca69 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/BaseUserQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/BaseUserQueries.java @@ -23,6 +23,7 @@ import com.djrapitops.plan.storage.database.queries.QueryAllStatement; import com.djrapitops.plan.storage.database.queries.QueryStatement; import com.djrapitops.plan.storage.database.sql.building.Select; import com.djrapitops.plan.storage.database.sql.tables.UsersTable; +import org.apache.commons.text.TextStringBuilder; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -160,4 +161,21 @@ public class BaseUserQueries { } }; } + + public static Query> fetchExistingUUIDs(Set outOfPlayerUUIDs) { + String sql = SELECT + UsersTable.USER_UUID + + FROM + UsersTable.TABLE_NAME + + WHERE + UsersTable.USER_UUID + " IN ('" + new TextStringBuilder().appendWithSeparators(outOfPlayerUUIDs, "','").build() + "')"; + + return new QueryAllStatement>(sql) { + @Override + public Set processResults(ResultSet set) throws SQLException { + Set uuids = new HashSet<>(); + while (set.next()) { + uuids.add(UUID.fromString(set.getString(UsersTable.USER_UUID))); + } + return uuids; + } + }; + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/ShutdownDataPreservationTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/ShutdownDataPreservationTransaction.java new file mode 100644 index 000000000..12483bf2f --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/ShutdownDataPreservationTransaction.java @@ -0,0 +1,83 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.storage.database.transactions.events; + +import com.djrapitops.plan.delivery.domain.PlayerName; +import com.djrapitops.plan.gathering.domain.FinishedSession; +import com.djrapitops.plan.gathering.domain.PlayerKill; +import com.djrapitops.plan.gathering.domain.PlayerKills; +import com.djrapitops.plan.storage.database.queries.LargeStoreQueries; +import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries; +import com.djrapitops.plan.storage.database.transactions.Transaction; + +import java.util.*; +import java.util.function.LongSupplier; + +public class ShutdownDataPreservationTransaction extends Transaction { + + private final List finishedSessions; + + public ShutdownDataPreservationTransaction(List finishedSessions) { + this.finishedSessions = finishedSessions; + } + + @Override + protected void performOperations() { + ensureAllPlayersAreRegistered(); + + execute(LargeStoreQueries.storeAllSessionsWithKillAndWorldData(finishedSessions)); + } + + private void ensureAllPlayersAreRegistered() { + Set playerUUIDs = new HashSet<>(); + Map playerNames = new HashMap<>(); + Map earliestDates = new HashMap<>(); + for (FinishedSession finishedSession : finishedSessions) { + UUID playerUUID = finishedSession.getPlayerUUID(); + playerUUIDs.add(playerUUID); + finishedSession.getExtraData(PlayerKills.class) + .map(PlayerKills::asList) + .ifPresent(kills -> { + for (PlayerKill kill : kills) { + playerUUIDs.add(kill.getKiller().getUuid()); + playerUUIDs.add(kill.getVictim().getUuid()); + } + }); + + finishedSession.getExtraData(PlayerName.class) + .map(PlayerName::get) + .ifPresent(playerName -> playerNames.put(playerUUID, playerName)); + long start = finishedSession.getStart(); + Long previous = earliestDates.get(playerUUID); + if (previous == null || start < previous) { + earliestDates.put(playerUUID, start); + } + } + + Set existingUUIDs = query(BaseUserQueries.fetchExistingUUIDs(playerUUIDs)); + + for (UUID playerUUID : playerUUIDs) { + if (!existingUUIDs.contains(playerUUID)) { + LongSupplier registerDate = () -> Optional.ofNullable(earliestDates.get(playerUUID)) + .orElseGet(System::currentTimeMillis); + String playerName = Optional.ofNullable(playerNames.get(playerUUID)) + .orElseGet(playerUUID::toString); + executeOther(new PlayerRegisterTransaction(playerUUID, registerDate, playerName)); + } + } + } +} diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/SessionQueriesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/SessionQueriesTest.java index 9d0ba4c83..c8be04b2c 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/SessionQueriesTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/SessionQueriesTest.java @@ -34,10 +34,7 @@ import com.djrapitops.plan.storage.database.transactions.ExecStatement; import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction; import com.djrapitops.plan.storage.database.transactions.Transaction; import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction; -import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction; -import com.djrapitops.plan.storage.database.transactions.events.PlayerServerRegisterTransaction; -import com.djrapitops.plan.storage.database.transactions.events.SessionEndTransaction; -import com.djrapitops.plan.storage.database.transactions.events.WorldNameStoreTransaction; +import com.djrapitops.plan.storage.database.transactions.events.*; import com.djrapitops.plan.utilities.java.Maps; import net.playeranalytics.plugin.scheduling.TimeAmount; import org.junit.jupiter.api.RepeatedTest; @@ -97,6 +94,45 @@ public interface SessionQueriesTest extends DatabaseTestPreparer { assertEquals(session, savedSessions.get(0)); } + @Test + default void shutdownDataPreservationTransactionOutOfOrderDoesNotFailDueToMissingMainUser() { + db().executeTransaction(new WorldNameStoreTransaction(serverUUID(), worlds[0])); + db().executeTransaction(new WorldNameStoreTransaction(serverUUID(), worlds[1])); + db().executeTransaction(new PlayerServerRegisterTransaction(player2UUID, RandomData::randomTime, + TestConstants.PLAYER_TWO_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); + + FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); + + db().executeTransaction(new ShutdownDataPreservationTransaction(List.of(session))); + + Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); + List savedSessions = sessions.get(serverUUID()); + + assertNotNull(savedSessions); + assertEquals(1, savedSessions.size()); + + assertEquals(session, savedSessions.get(0)); + } + + @Test + default void shutdownDataPreservationTransactionOutOfOrderDoesNotFailDueToMissingKilledUser() { + db().executeTransaction(new WorldNameStoreTransaction(serverUUID(), worlds[0])); + db().executeTransaction(new WorldNameStoreTransaction(serverUUID(), worlds[1])); + db().executeTransaction(new PlayerServerRegisterTransaction(playerUUID, RandomData::randomTime, + TestConstants.PLAYER_ONE_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); + + FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); + db().executeTransaction(new ShutdownDataPreservationTransaction(List.of(session))); + + Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); + List savedSessions = sessions.get(serverUUID()); + + assertNotNull(savedSessions); + assertEquals(1, savedSessions.size()); + + assertEquals(session, savedSessions.get(0)); + } + @Test default void killsAreAvailableAfter2ndUserRegisterEvenIfOutOfOrder() { db().executeTransaction(new WorldNameStoreTransaction(serverUUID(), worlds[0]));