Transaction for removing unsatisfied Conditional data:

This is one of the most complex queries I have made.

- Select all fulfilled conditions for all players (conditionName when
  true and not_conditionName when false)
- Left join with player value & provider tables when uuids match, and
  when condition matches a condition in the query above.
- Filter the join query for values where the condition did not match
  any provided condition in the join (Is null)
- Delete all player values with IDs that are returned by the left join
  query after filtering

In addition:
- Added test for the transaction
- Added extension data removal to RemoveEverythingTransaction
- Added unregister method to ExtensionService
This commit is contained in:
Rsl1122 2019-03-26 12:22:57 +02:00
parent 09ac2dce09
commit 47a6a9b2aa
10 changed files with 224 additions and 1 deletions

View File

@ -58,6 +58,16 @@ public interface ExtensionService {
*/
void register(DataExtension extension);
/**
* Unregister your {@link DataExtension} implementation.
* <p>
* This method should be used if calling methods on the DataExtension suddenly becomes unavailable, due to
* plugin disable for example.
*
* @param extension Your DataExtension implementation that was registered before.
*/
void unregister(DataExtension extension);
class ExtensionServiceHolder {
static ExtensionService API;

View File

@ -26,6 +26,7 @@ import com.djrapitops.plan.db.access.transactions.init.CreateIndexTransaction;
import com.djrapitops.plan.db.access.transactions.init.CreateTablesTransaction;
import com.djrapitops.plan.db.access.transactions.init.OperationCriticalTransaction;
import com.djrapitops.plan.db.patches.*;
import com.djrapitops.plan.system.DebugChannels;
import com.djrapitops.plan.system.locale.Locale;
import com.djrapitops.plan.system.settings.config.PlanConfig;
import com.djrapitops.plan.system.settings.paths.TimeSettings;
@ -219,6 +220,7 @@ public abstract class SQLDB extends AbstractDatabase {
return CompletableFuture.supplyAsync(() -> {
accessLock.checkAccess(transaction);
logger.getDebugLogger().logOn(DebugChannels.SQL, "Executing: " + transaction.getClass().getSimpleName());
transaction.executeTransaction(this);
return CompletableFuture.completedFuture(null);
}, getTransactionExecutor()).handle(errorHandler(origin));

View File

@ -44,6 +44,11 @@ public class RemoveEverythingTransaction extends Transaction {
clearTable(TPSTable.TABLE_NAME);
clearTable(SecurityTable.TABLE_NAME);
clearTable(ServerTable.TABLE_NAME);
clearTable(ExtensionPlayerValueTable.TABLE_NAME);
clearTable(ExtensionProviderTable.TABLE_NAME);
clearTable(ExtensionTabTable.TABLE_NAME);
clearTable(ExtensionPluginTable.TABLE_NAME);
clearTable(ExtensionIconTable.TABLE_NAME);
}
private void clearTable(String tableName) {

View File

@ -27,6 +27,7 @@ import com.djrapitops.plan.db.access.transactions.commands.RemovePlayerTransacti
import com.djrapitops.plan.db.sql.tables.PingTable;
import com.djrapitops.plan.db.sql.tables.SessionsTable;
import com.djrapitops.plan.db.sql.tables.TPSTable;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.RemoveUnsatisfiedConditionalResultsTransaction;
import com.djrapitops.plan.system.locale.Locale;
import com.djrapitops.plan.system.locale.lang.PluginLang;
import com.djrapitops.plugin.api.TimeAmount;
@ -73,6 +74,9 @@ public class CleanTransaction extends Transaction {
execute(cleanTPSTable(allTimePeak.orElse(-1)));
execute(cleanPingTable());
// Clean DataExtension data
executeOther(new RemoveUnsatisfiedConditionalResultsTransaction());
int removed = cleanOldPlayers();
if (removed > 0) {
logger.info(locale.getString(PluginLang.DB_NOTIFY_CLEAN, removed));

View File

@ -31,6 +31,7 @@ public class Sql {
public static final String INNER_JOIN = " INNER JOIN ";
public static final String LEFT_JOIN = " LEFT JOIN ";
public static final String AND = " AND ";
public static final String OR = " OR ";
private Sql() {
throw new IllegalStateException("Variable Class");

View File

@ -95,6 +95,16 @@ public class ExtensionServiceImplementation implements ExtensionService {
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, pluginName + " extension registered.");
}
@Override
public void unregister(DataExtension extension) {
DataProviderExtractor extractor = new DataProviderExtractor(extension);
String pluginName = extractor.getPluginName();
if (extensionGatherers.remove(pluginName) != null) {
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, pluginName + " extension unregistered.");
}
}
private boolean shouldNotAllowRegistration(String pluginName) {
PluginsConfigSection pluginsConfig = config.getPluginsConfigSection();
@ -118,7 +128,11 @@ public class ExtensionServiceImplementation implements ExtensionService {
public void updatePlayerValues(UUID playerUUID, String playerName) {
for (Map.Entry<String, ProviderValueGatherer> gatherer : extensionGatherers.entrySet()) {
try {
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, "Gathering values for: " + playerName);
gatherer.getValue().updateValues(playerUUID, playerName);
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, "Gathering completed: " + playerName);
} catch (Exception | NoClassDefFoundError | NoSuchMethodError | NoSuchFieldError e) {
logger.warn(gatherer.getKey() + " ran into (but failed safely) " + e.getClass().getSimpleName() +
" when updating value for '" + playerName +

View File

@ -75,7 +75,6 @@ public class ProviderValueGatherer {
}
database.executeTransaction(new RemoveInvalidResultsTransaction(pluginName, serverUUID, extractor.getInvalidatedMethods()));
// TODO remove data in db that are updated with a 'false' condition
}
public void updateValues(UUID playerUUID, String playerName) {

View File

@ -135,4 +135,11 @@ public class ExtensionTabData implements Comparable<ExtensionTabData> {
return data;
}
}
@Override
public String toString() {
return "ExtensionTabData{" +
"available=" + order +
'}';
}
}

View File

@ -0,0 +1,96 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.extension.implementation.storage.transactions.results;
import com.djrapitops.plan.db.DBType;
import com.djrapitops.plan.db.access.ExecStatement;
import com.djrapitops.plan.db.access.Executable;
import com.djrapitops.plan.db.access.transactions.Transaction;
import com.djrapitops.plan.db.sql.tables.ExtensionPlayerValueTable;
import com.djrapitops.plan.db.sql.tables.ExtensionProviderTable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import static com.djrapitops.plan.db.sql.parsing.Sql.*;
/**
* Transaction to remove older results that violate an updated condition value.
* <p>
* How it works:
* - Select all fulfilled conditions for all players (conditionName when true and not_conditionName when false)
* - Left join with player value & provider tables when uuids match, and when condition matches a condition in the query above.
* - Filter the join query for values where the condition did not match any provided condition in the join (Is null)
* - Delete all player values with IDs that are returned by the left join query after filtering
*
* @author Rsl1122
*/
public class RemoveUnsatisfiedConditionalResultsTransaction extends Transaction {
@Override
protected void performOperations() {
execute(deleteUnsatisfied());
}
private Executable deleteUnsatisfied() {
String reversedCondition = dbType == DBType.SQLITE ? "'not_' || " + ExtensionProviderTable.PROVIDED_CONDITION : "CONCAT('not_'," + ExtensionProviderTable.PROVIDED_CONDITION + ')';
String providerTable = ExtensionProviderTable.TABLE_NAME;
String playerValueTable = ExtensionPlayerValueTable.TABLE_NAME;
String selectSatisfiedPositiveConditions = SELECT +
ExtensionProviderTable.PROVIDED_CONDITION + ',' +
ExtensionPlayerValueTable.USER_UUID +
FROM + playerValueTable +
INNER_JOIN + providerTable + " on " + providerTable + '.' + ExtensionProviderTable.ID + "=" + ExtensionPlayerValueTable.PROVIDER_ID +
WHERE + ExtensionPlayerValueTable.BOOLEAN_VALUE + "=?" +
AND + ExtensionProviderTable.PROVIDED_CONDITION + " IS NOT NULL";
String selectSatisfiedNegativeConditions = SELECT +
reversedCondition + " as " + ExtensionProviderTable.PROVIDED_CONDITION + ',' +
ExtensionPlayerValueTable.USER_UUID +
FROM + playerValueTable +
INNER_JOIN + providerTable + " on " + providerTable + '.' + ExtensionProviderTable.ID + "=" + ExtensionPlayerValueTable.PROVIDER_ID +
WHERE + ExtensionPlayerValueTable.BOOLEAN_VALUE + "=?" +
AND + ExtensionProviderTable.PROVIDED_CONDITION + " IS NOT NULL";
String selectSatisfiedConditions = '(' + selectSatisfiedPositiveConditions + " UNION " + selectSatisfiedNegativeConditions + ") q1";
String selectUnsatisfiedValueIDs = SELECT + playerValueTable + '.' + ExtensionPlayerValueTable.ID +
FROM + playerValueTable +
INNER_JOIN + providerTable + " on " + providerTable + '.' + ExtensionProviderTable.ID + "=" + ExtensionPlayerValueTable.PROVIDER_ID +
LEFT_JOIN + selectSatisfiedConditions + // Left join to preserver values that don't have their condition fulfilled
" on (" +
playerValueTable + '.' + ExtensionPlayerValueTable.USER_UUID +
"=q1." + ExtensionPlayerValueTable.USER_UUID +
AND + ExtensionProviderTable.CONDITION +
"=q1." + ExtensionProviderTable.PROVIDED_CONDITION +
')' +
WHERE + "q1." + ExtensionProviderTable.PROVIDED_CONDITION + " IS NULL" + // Conditions that were not in the satisfied condition query
AND + ExtensionProviderTable.CONDITION + " IS NOT NULL"; // Ignore values that don't need condition
String sql = "DELETE FROM " + playerValueTable +
WHERE + ExtensionPlayerValueTable.ID + " IN (" + selectUnsatisfiedValueIDs + ')';
return new ExecStatement(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setBoolean(1, true); // Select provided conditions with 'true' value
statement.setBoolean(2, false); // Select negated conditions with 'false' value
}
};
}
}

View File

@ -52,6 +52,7 @@ import com.djrapitops.plan.extension.implementation.results.player.ExtensionPlay
import com.djrapitops.plan.extension.implementation.results.player.ExtensionStringData;
import com.djrapitops.plan.extension.implementation.results.player.ExtensionTabData;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionPlayerDataQuery;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.RemoveUnsatisfiedConditionalResultsTransaction;
import com.djrapitops.plan.system.PlanSystem;
import com.djrapitops.plan.system.database.DBSystem;
import com.djrapitops.plan.system.info.server.Server;
@ -79,6 +80,7 @@ import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -153,6 +155,9 @@ public abstract class CommonDBTest {
db.executeTransaction(new StoreServerInformationTransaction(new Server(-1, serverUUID, "ServerName", "", 20)));
assertEquals(serverUUID, db.getServerUUIDSupplier().get());
system.getExtensionService().unregister(new TestExtension());
system.getExtensionService().unregister(new ConditionalExtension());
}
private void execute(Executable executable) {
@ -1051,6 +1056,58 @@ public abstract class CommonDBTest {
OptionalAssert.equals("Something", tabData.getString("stringVal").map(ExtensionStringData::getFormattedValue));
}
@Test
public void unsatisfiedConditionalResultsAreCleaned() throws ExecutionException, InterruptedException {
ExtensionServiceImplementation extensionService = (ExtensionServiceImplementation) system.getExtensionService();
extensionService.register(new ConditionalExtension());
ConditionalExtension.condition = true;
extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME);
// Check that the wanted data exists
checkThatDataExists(ConditionalExtension.condition);
// Reverse condition
ConditionalExtension.condition = false;
extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME);
db.executeTransaction(new RemoveUnsatisfiedConditionalResultsTransaction());
// Check that the wanted data exists
checkThatDataExists(ConditionalExtension.condition);
// Reverse condition
ConditionalExtension.condition = false;
extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME);
db.executeTransaction(new RemoveUnsatisfiedConditionalResultsTransaction());
// Check that the wanted data exists
checkThatDataExists(ConditionalExtension.condition);
}
private void checkThatDataExists(boolean condition) {
if (condition) { // Condition is true, conditional values exist
List<ExtensionPlayerData> ofServer = db.query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID);
assertTrue("There was no data left", ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty());
ExtensionTabData tabData = ofServer.get(0).getTabs().get(0);
OptionalAssert.equals("Yes", tabData.getBoolean("isCondition").map(ExtensionBooleanData::getFormattedValue));
OptionalAssert.equals("Conditional", tabData.getString("conditionalValue").map(ExtensionStringData::getFormattedValue));
OptionalAssert.equals("unconditional", tabData.getString("unconditional").map(ExtensionStringData::getFormattedValue)); // Was not removed
assertFalse("Value was not removed: reversedConditionalValue", tabData.getString("reversedConditionalValue").isPresent());
} else { // Condition is false, reversed conditional values exist
List<ExtensionPlayerData> ofServer = db.query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID);
assertTrue("There was no data left", ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty());
ExtensionTabData tabData = ofServer.get(0).getTabs().get(0);
OptionalAssert.equals("No", tabData.getBoolean("isCondition").map(ExtensionBooleanData::getFormattedValue));
OptionalAssert.equals("Reversed", tabData.getString("reversedConditionalValue").map(ExtensionStringData::getFormattedValue));
OptionalAssert.equals("unconditional", tabData.getString("unconditional").map(ExtensionStringData::getFormattedValue)); // Was not removed
assertFalse("Value was not removed: conditionalValue", tabData.getString("conditionalValue").isPresent());
}
}
@PluginInfo(name = "TestExtension")
public class TestExtension implements DataExtension {
@NumberProvider(text = "a number")
@ -1078,4 +1135,32 @@ public abstract class CommonDBTest {
return "Something";
}
}
@PluginInfo(name = "Conditional TestExtension")
public static class ConditionalExtension implements DataExtension {
static boolean condition = true;
@BooleanProvider(text = "a boolean", conditionName = "condition")
public boolean isCondition(UUID playerUUID) {
return condition;
}
@StringProvider(text = "Conditional Value")
@Conditional("condition")
public String conditionalValue(UUID playerUUID) {
return "Conditional";
}
@StringProvider(text = "Reversed Conditional Value")
@Conditional(value = "condition", negated = true)
public String reversedConditionalValue(UUID playerUUID) {
return "Reversed";
}
@StringProvider(text = "Unconditional")
public String unconditional(UUID playerUUID) {
return "unconditional";
}
}
}