Plan/Plan/common/src/main/java/com/djrapitops/plan/storage/upkeep/DBCleanTask.java

203 lines
9.3 KiB
Java

/*
* 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.storage.upkeep;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.query.QuerySvc;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.TimeSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
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.Database;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryStatement;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.sql.tables.SessionsTable;
import com.djrapitops.plan.storage.database.sql.tables.UsersTable;
import com.djrapitops.plan.storage.database.transactions.commands.RemovePlayerTransaction;
import com.djrapitops.plan.storage.database.transactions.init.RemoveDuplicateUserInfoTransaction;
import com.djrapitops.plan.storage.database.transactions.init.RemoveOldAccessLogTransaction;
import com.djrapitops.plan.storage.database.transactions.init.RemoveOldExtensionsTransaction;
import com.djrapitops.plan.storage.database.transactions.init.RemoveOldSampledDataTransaction;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.scheduling.PluginRunnable;
import net.playeranalytics.plugin.scheduling.RunnableFactory;
import net.playeranalytics.plugin.scheduling.TimeAmount;
import net.playeranalytics.plugin.server.PluginLogger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Task for cleaning the active database.
*
* @author AuroraLS3
*/
@Singleton
public class DBCleanTask extends TaskSystem.Task {
private final Locale locale;
private final DBSystem dbSystem;
private final PlanConfig config;
private final QuerySvc queryService;
private final ServerInfo serverInfo;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
// This variable assumes that the system is thrown away on reload and new one is constructed.
// It is to avoid cleaning extension data that has not been updated after uptime longer than the deletion threshold.
private final long lastReload;
@Inject
public DBCleanTask(
PlanConfig config,
Locale locale,
DBSystem dbSystem,
QuerySvc queryService,
ServerInfo serverInfo,
PluginLogger logger,
ErrorLogger errorLogger
) {
this.locale = locale;
this.dbSystem = dbSystem;
this.config = config;
this.queryService = queryService;
this.serverInfo = serverInfo;
this.logger = logger;
this.errorLogger = errorLogger;
lastReload = System.currentTimeMillis();
}
@Override
public void run() {
Database database = dbSystem.getDatabase();
try {
if (database.getState() != Database.State.CLOSED) {
database.executeTransaction(new RemoveOldAccessLogTransaction(TimeUnit.DAYS.toMillis(config.get(WebserverSettings.REMOVE_ACCESS_LOG_AFTER_DAYS))));
database.executeTransaction(new RemoveOldSampledDataTransaction(
serverInfo.getServerUUID(),
config.get(TimeSettings.DELETE_TPS_DATA_AFTER),
config.get(TimeSettings.DELETE_PING_DATA_AFTER)
));
database.executeTransaction(new RemoveDuplicateUserInfoTransaction());
int removed = cleanOldPlayers(database);
if (removed > 0) {
logger.info(locale.getString(PluginLang.DB_NOTIFY_CLEAN, removed));
}
Long deleteExtensionDataAfter = config.get(TimeSettings.DELETE_EXTENSION_DATA_AFTER);
Long databaseCleanPeriod = config.get(TimeSettings.CLEAN_DATABASE_PERIOD);
if (databaseCleanPeriod > deleteExtensionDataAfter) {
logger.warn("Data of Disabled Extensions can not be cleaned due to " + TimeSettings.CLEAN_DATABASE_PERIOD.getPath() + " being larger than " + TimeSettings.DELETE_EXTENSION_DATA_AFTER.getPath());
}
// Avoid cleaning extension data that has not been updated after uptime longer than the deletion threshold.
// This is needed since the last updated number is updated at reload and it would lead to all data
// for plugins being deleted all the time.
if (System.currentTimeMillis() - lastReload <= deleteExtensionDataAfter) {
database.executeTransaction(new RemoveOldExtensionsTransaction(config.getExtensionSettings(), deleteExtensionDataAfter, serverInfo.getServerUUID()));
}
}
} catch (DBOpException e) {
errorLogger.error(e);
cancel();
}
}
@Override
public void register(RunnableFactory runnableFactory) {
PluginRunnable taskToRegister = this;
// Secondary task for registration due to database queries.
runnableFactory.create(() -> {
// Distribute clean task evenly between multiple servers.
// see https://github.com/plan-player-analytics/Plan/issues/1641 for why
Integer biggestId = dbSystem.getDatabase().query(ServerQueries.fetchBiggestServerID());
Integer id = serverInfo.getServer().getId().orElse(1);
double distributor = id * 1.0 / biggestId; // 0 < distributor <= 1
long distributingOverTimeMs = config.get(TimeSettings.CLEAN_DATABASE_PERIOD);
// -40 seconds to start first at 20 seconds if only one server is present and period is 1 minute.
long startAfterMs = (long) (distributor * distributingOverTimeMs) - TimeUnit.SECONDS.toMillis(40L);
long delayTicks = TimeAmount.toTicks(startAfterMs, TimeUnit.MILLISECONDS);
long periodTicks = TimeAmount.toTicks(config.get(TimeSettings.CLEAN_DATABASE_PERIOD), TimeUnit.MILLISECONDS);
runnableFactory.create(taskToRegister).runTaskTimerAsynchronously(delayTicks, periodTicks);
}).runTaskAsynchronously();
}
// VisibleForTesting
public int cleanOldPlayers(Database database) {
// Only clean if this is a proxy server or no proxy servers are installed.
if (serverInfo.getServer().isProxy() || database.query(ServerQueries.fetchProxyServers()).isEmpty()) {
long now = System.currentTimeMillis();
long keepActiveAfter = now - config.get(TimeSettings.DELETE_INACTIVE_PLAYERS_AFTER);
List<UUID> inactivePlayers = database.query(fetchInactivePlayerUUIDs(keepActiveAfter));
for (UUID playerUUID : inactivePlayers) {
queryService.playerRemoved(playerUUID);
database.executeTransaction(new RemovePlayerTransaction(playerUUID));
}
return inactivePlayers.size();
}
// Skip cleaning on game servers if proxy server is installed.
return 0;
}
private Query<List<UUID>> fetchInactivePlayerUUIDs(long keepActiveAfter) {
String selectLastSeen = SELECT + "MAX(" + SessionsTable.SESSION_END + ") as last_seen, " +
SessionsTable.USER_ID +
FROM + SessionsTable.TABLE_NAME +
GROUP_BY + SessionsTable.USER_ID;
String sql = SELECT + "uuid, last_seen" +
FROM + '(' + selectLastSeen + ") as q1" +
INNER_JOIN + UsersTable.TABLE_NAME + " u on u." + UsersTable.ID + '=' + "q1." + SessionsTable.USER_ID +
WHERE + "last_seen < ?";
return new QueryStatement<>(sql, 20000) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, keepActiveAfter);
}
@Override
public List<UUID> processResults(ResultSet set) throws SQLException {
List<UUID> inactiveUUIDs = new ArrayList<>();
while (set.next()) {
inactiveUUIDs.add(UUID.fromString(set.getString("uuid")));
}
return inactiveUUIDs;
}
};
}
}