mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-10-06 02:17:39 +02:00
* ShutdownHook: No sessions to save check ShutdownHook now checks if it needs to save any sessions and does not start the database if no sessions are unsaved. * SessionCache.getActiveSessions() now immutable * [#769] Bukkit and Sponge server shutdown save Implemented following save procedure for Bukkit: - On plugin disable check if server is shutting down and save sessions - Shutdown hook triggered on JVM shutdown calls the same session save - Save clears sessions from cache, so the sessions are not saved twice Implemented following save procedure for Sponge: - Listen for GameStoppingServerEvent - On plugin disable ask listener if shutting down and save sessions - Shutdown hook triggered on JVM shutdown calls the same session save - Save clears sessions from cache, so the sessions are not saved twice Test: - Tests ShutdownSave on reload - Tests ShutdownSave on shutdown - Tests ShutdownSave on JVM shutdown
This commit is contained in:
parent
96564c90be
commit
16e6ef1dc7
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plan.utilities.java.Reflection;
|
||||
import com.djrapitops.plugin.logging.console.PluginLogger;
|
||||
import com.djrapitops.plugin.logging.error.ErrorHandler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* ServerShutdownSave implementation for Bukkit based servers.
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@Singleton
|
||||
public class BukkitServerShutdownSave extends ServerShutdownSave {
|
||||
|
||||
private final PluginLogger logger;
|
||||
|
||||
@Inject
|
||||
public BukkitServerShutdownSave(
|
||||
DBSystem dbSystem,
|
||||
PluginLogger logger,
|
||||
ErrorHandler errorHandler
|
||||
) {
|
||||
super(dbSystem, errorHandler);
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkServerShuttingDownStatus() {
|
||||
try {
|
||||
return performCheck();
|
||||
} catch (Exception | NoClassDefFoundError | NoSuchFieldError e) {
|
||||
logger.debug("Server shutdown check failed, using JVM ShutdownHook instead. Error: " + e.toString());
|
||||
return false; // ShutdownHook handles save in case this fails upon plugin disable.
|
||||
}
|
||||
}
|
||||
|
||||
private boolean performCheck() {
|
||||
// Special thanks to Fuzzlemann for figuring out the methods required for this check.
|
||||
// https://github.com/Rsl1122/Plan-PlayerAnalytics/issues/769#issuecomment-433898242
|
||||
Class<?> minecraftServerClass = Reflection.getMinecraftClass("MinecraftServer");
|
||||
Object minecraftServer = Reflection.getField(minecraftServerClass, "SERVER", minecraftServerClass).get(null);
|
||||
|
||||
return Reflection.getField(minecraftServerClass, "isStopped", boolean.class).get(minecraftServer);
|
||||
}
|
||||
}
|
@ -40,6 +40,7 @@ public class Plan extends BukkitPlugin implements PlanPlugin {
|
||||
|
||||
private PlanSystem system;
|
||||
private Locale locale;
|
||||
private ServerShutdownSave serverShutdownSave;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@ -47,6 +48,7 @@ public class Plan extends BukkitPlugin implements PlanPlugin {
|
||||
try {
|
||||
timings.start("Enable");
|
||||
system = component.system();
|
||||
serverShutdownSave = component.serverShutdownSave();
|
||||
locale = system.getLocaleSystem().getLocale();
|
||||
system.enable();
|
||||
|
||||
@ -88,6 +90,10 @@ public class Plan extends BukkitPlugin implements PlanPlugin {
|
||||
*/
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (serverShutdownSave != null) {
|
||||
logger.info(locale != null ? locale.getString(PluginLang.DISABLED_UNSAVED_SESSIONS) : PluginLang.DISABLED_UNSAVED_SESSIONS.getDefault());
|
||||
serverShutdownSave.performSave();
|
||||
}
|
||||
if (system != null) {
|
||||
system.disable();
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ public interface PlanBukkitComponent {
|
||||
|
||||
PlanSystem system();
|
||||
|
||||
ServerShutdownSave serverShutdownSave();
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan.modules.bukkit;
|
||||
|
||||
import com.djrapitops.plan.BukkitServerShutdownSave;
|
||||
import com.djrapitops.plan.ServerShutdownSave;
|
||||
import com.djrapitops.plan.system.database.BukkitDBSystem;
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plan.system.importing.BukkitImportSystem;
|
||||
@ -55,6 +57,9 @@ public interface BukkitSuperClassBindingModule {
|
||||
ListenerSystem bindBukkitListenerSystem(BukkitListenerSystem bukkitListenerSystem);
|
||||
|
||||
@Binds
|
||||
ImportSystem bindImportSsytem(BukkitImportSystem bukkitImportSystem);
|
||||
ImportSystem bindImportSystem(BukkitImportSystem bukkitImportSystem);
|
||||
|
||||
@Binds
|
||||
ServerShutdownSave bindBukkitServerShutdownSave(BukkitServerShutdownSave bukkitServerShutdownSave);
|
||||
|
||||
}
|
@ -33,6 +33,7 @@ import com.djrapitops.plugin.task.RunnableFactory;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -41,6 +42,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@Singleton
|
||||
public class BukkitTaskSystem extends ServerTaskSystem {
|
||||
|
||||
private final Plan plugin;
|
||||
|
@ -28,6 +28,7 @@ import com.djrapitops.plugin.api.TimeAmount;
|
||||
import com.djrapitops.plugin.task.RunnableFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -35,6 +36,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@Singleton
|
||||
public class BungeeTaskSystem extends TaskSystem {
|
||||
|
||||
private final PlanBungee plugin;
|
||||
|
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.djrapitops.plan.api.exceptions.database.DBInitException;
|
||||
import com.djrapitops.plan.api.exceptions.database.DBOpException;
|
||||
import com.djrapitops.plan.data.container.Session;
|
||||
import com.djrapitops.plan.data.store.keys.SessionKeys;
|
||||
import com.djrapitops.plan.db.Database;
|
||||
import com.djrapitops.plan.db.access.transactions.events.ServerShutdownTransaction;
|
||||
import com.djrapitops.plan.system.cache.SessionCache;
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plugin.logging.L;
|
||||
import com.djrapitops.plugin.logging.error.ErrorHandler;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Class in charge of performing save operations when the server shuts down.
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public abstract class ServerShutdownSave {
|
||||
|
||||
private final DBSystem dbSystem;
|
||||
private final ErrorHandler errorHandler;
|
||||
private boolean shuttingDown = false;
|
||||
|
||||
public ServerShutdownSave(
|
||||
DBSystem dbSystem,
|
||||
ErrorHandler errorHandler
|
||||
) {
|
||||
this.dbSystem = dbSystem;
|
||||
this.errorHandler = errorHandler;
|
||||
}
|
||||
|
||||
protected abstract boolean checkServerShuttingDownStatus();
|
||||
|
||||
public void serverIsKnownToBeShuttingDown() {
|
||||
shuttingDown = true;
|
||||
}
|
||||
|
||||
public void performSave() {
|
||||
if (!checkServerShuttingDownStatus() && !shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<UUID, Session> activeSessions = SessionCache.getActiveSessions();
|
||||
if (activeSessions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
attemptSave(activeSessions);
|
||||
|
||||
SessionCache.clear();
|
||||
}
|
||||
|
||||
private void attemptSave(Map<UUID, Session> activeSessions) {
|
||||
try {
|
||||
prepareSessionsForStorage(activeSessions, System.currentTimeMillis());
|
||||
saveActiveSessions(activeSessions);
|
||||
} catch (DBInitException e) {
|
||||
errorHandler.log(L.ERROR, this.getClass(), e);
|
||||
} catch (IllegalStateException ignored) {
|
||||
/* Database is not initialized */
|
||||
} finally {
|
||||
closeDatabase(dbSystem.getDatabase());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveActiveSessions(Map<UUID, Session> activeSessions) {
|
||||
Database database = dbSystem.getDatabase();
|
||||
if (database.getState() == Database.State.CLOSED) {
|
||||
// Ensure that database is not closed when performing the transaction.
|
||||
database.init();
|
||||
}
|
||||
|
||||
saveSessions(activeSessions, database);
|
||||
}
|
||||
|
||||
private void prepareSessionsForStorage(Map<UUID, Session> activeSessions, long now) {
|
||||
for (Session session : activeSessions.values()) {
|
||||
Optional<Long> end = session.getValue(SessionKeys.END);
|
||||
if (!end.isPresent()) {
|
||||
session.endSession(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSessions(Map<UUID, Session> activeSessions, Database database) {
|
||||
try {
|
||||
database.executeTransaction(new ServerShutdownTransaction(activeSessions.values()))
|
||||
.get(); // Ensure that the transaction is executed before shutdown.
|
||||
} catch (ExecutionException | DBOpException e) {
|
||||
errorHandler.log(L.ERROR, this.getClass(), e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDatabase(Database database) {
|
||||
database.close();
|
||||
}
|
||||
}
|
@ -16,21 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan;
|
||||
|
||||
import com.djrapitops.plan.api.exceptions.database.DBInitException;
|
||||
import com.djrapitops.plan.api.exceptions.database.DBOpException;
|
||||
import com.djrapitops.plan.data.container.Session;
|
||||
import com.djrapitops.plan.data.store.keys.SessionKeys;
|
||||
import com.djrapitops.plan.db.Database;
|
||||
import com.djrapitops.plan.db.access.transactions.events.ServerShutdownTransaction;
|
||||
import com.djrapitops.plan.system.cache.SessionCache;
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plugin.logging.L;
|
||||
import com.djrapitops.plugin.logging.error.ErrorHandler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Thread that is run when JVM shuts down.
|
||||
@ -39,16 +26,16 @@ import java.util.UUID;
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@Singleton
|
||||
public class ShutdownHook extends Thread {
|
||||
|
||||
private static ShutdownHook activated;
|
||||
private final DBSystem dbSystem;
|
||||
private final ErrorHandler errorHandler;
|
||||
|
||||
private final ServerShutdownSave serverShutdownSave;
|
||||
|
||||
@Inject
|
||||
public ShutdownHook(DBSystem dbSystem, ErrorHandler errorHandler) {
|
||||
this.dbSystem = dbSystem;
|
||||
this.errorHandler = errorHandler;
|
||||
public ShutdownHook(ServerShutdownSave serverShutdownSave) {
|
||||
this.serverShutdownSave = serverShutdownSave;
|
||||
}
|
||||
|
||||
private static boolean isActivated() {
|
||||
@ -74,47 +61,7 @@ public class ShutdownHook extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Map<UUID, Session> activeSessions = SessionCache.getActiveSessions();
|
||||
prepareSessionsForStorage(activeSessions, System.currentTimeMillis());
|
||||
saveActiveSessions(activeSessions);
|
||||
} catch (DBInitException e) {
|
||||
errorHandler.log(L.ERROR, this.getClass(), e);
|
||||
} catch (IllegalStateException ignored) {
|
||||
/* Database is not initialized */
|
||||
} finally {
|
||||
closeDatabase(dbSystem.getDatabase());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveActiveSessions(Map<UUID, Session> activeSessions) {
|
||||
Database database = dbSystem.getDatabase();
|
||||
if (database.getState() == Database.State.CLOSED) {
|
||||
// Ensure that database is not closed when performing the transaction.
|
||||
database.init();
|
||||
}
|
||||
|
||||
saveSessions(activeSessions, database);
|
||||
}
|
||||
|
||||
private void prepareSessionsForStorage(Map<UUID, Session> activeSessions, long now) {
|
||||
for (Session session : activeSessions.values()) {
|
||||
Optional<Long> end = session.getValue(SessionKeys.END);
|
||||
if (!end.isPresent()) {
|
||||
session.endSession(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSessions(Map<UUID, Session> activeSessions, Database database) {
|
||||
try {
|
||||
database.executeTransaction(new ServerShutdownTransaction(activeSessions.values()));
|
||||
} catch (DBOpException e) {
|
||||
errorHandler.log(L.ERROR, this.getClass(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDatabase(Database database) {
|
||||
database.close();
|
||||
serverShutdownSave.serverIsKnownToBeShuttingDown();
|
||||
serverShutdownSave.performSave();
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,8 @@ public class SQLiteDB extends SQLDB {
|
||||
@Override
|
||||
public void setupDataSource() {
|
||||
try {
|
||||
if (connection != null) connection.close();
|
||||
|
||||
connection = getNewConnection(databaseFile);
|
||||
} catch (SQLException e) {
|
||||
throw new DBInitException(e.getMessage(), e);
|
||||
@ -139,11 +141,12 @@ public class SQLiteDB extends SQLDB {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
logger.debug("SQLite Connection close prompted by: " + ThrowableUtils.findCallerAfterClass(Thread.currentThread().getStackTrace(), SQLiteDB.class));
|
||||
|
||||
super.close();
|
||||
stopConnectionPingTask();
|
||||
|
||||
if (connection != null) {
|
||||
logger.debug("SQLite Connection close prompted by: " + ThrowableUtils.findCallerAfterClass(Thread.currentThread().getStackTrace(), SQLiteDB.class));
|
||||
logger.debug("SQLite " + dbName + ": Closed Connection");
|
||||
MiscUtils.close(connection);
|
||||
}
|
||||
|
@ -30,11 +30,17 @@ import javax.inject.Singleton;
|
||||
@Singleton
|
||||
public class CacheSystem implements SubSystem {
|
||||
|
||||
private final SessionCache sessionCache;
|
||||
private final NicknameCache nicknameCache;
|
||||
private final GeolocationCache geolocationCache;
|
||||
|
||||
@Inject
|
||||
public CacheSystem(NicknameCache nicknameCache, GeolocationCache geolocationCache) {
|
||||
public CacheSystem(
|
||||
SessionCache sessionCache,
|
||||
NicknameCache nicknameCache,
|
||||
GeolocationCache geolocationCache
|
||||
) {
|
||||
this.sessionCache = sessionCache;
|
||||
this.nicknameCache = nicknameCache;
|
||||
this.geolocationCache = geolocationCache;
|
||||
}
|
||||
@ -58,4 +64,7 @@ public class CacheSystem implements SubSystem {
|
||||
return geolocationCache;
|
||||
}
|
||||
|
||||
public SessionCache getSessionCache() {
|
||||
return sessionCache;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package com.djrapitops.plan.system.cache;
|
||||
|
||||
import com.djrapitops.plan.data.container.Session;
|
||||
import com.djrapitops.plan.data.store.keys.SessionKeys;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -42,7 +43,7 @@ public class SessionCache {
|
||||
}
|
||||
|
||||
public static Map<UUID, Session> getActiveSessions() {
|
||||
return ACTIVE_SESSIONS;
|
||||
return ImmutableMap.copyOf(ACTIVE_SESSIONS);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
|
@ -49,6 +49,7 @@ public enum PluginLang implements Lang {
|
||||
DISABLED_WEB_SERVER("Disable - WebServer", "Webserver has been disabled."),
|
||||
DISABLED_PROCESSING("Disable - Processing", "Processing critical unprocessed tasks. (${0})"),
|
||||
DISABLED_PROCESSING_COMPLETE("Disable - Processing Complete", "Processing complete."),
|
||||
DISABLED_UNSAVED_SESSIONS("Disable - Unsaved Session Save", "Saving unfinished sessions.."),
|
||||
|
||||
VERSION_NEWEST("Version - Latest", "You're using the latest version."),
|
||||
VERSION_AVAILABLE("Version - New", "New Release (${0}) is available ${1}"),
|
||||
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
package com.djrapitops.plan.system.processing;
|
||||
|
||||
import com.djrapitops.plan.api.exceptions.EnableException;
|
||||
import com.djrapitops.plan.system.SubSystem;
|
||||
import com.djrapitops.plan.system.locale.Locale;
|
||||
import com.djrapitops.plan.system.locale.lang.PluginLang;
|
||||
@ -38,8 +37,8 @@ public class Processing implements SubSystem {
|
||||
private final PluginLogger logger;
|
||||
private final ErrorHandler errorHandler;
|
||||
|
||||
private final ExecutorService nonCriticalExecutor;
|
||||
private final ExecutorService criticalExecutor;
|
||||
private ExecutorService nonCriticalExecutor;
|
||||
private ExecutorService criticalExecutor;
|
||||
|
||||
@Inject
|
||||
public Processing(
|
||||
@ -50,8 +49,12 @@ public class Processing implements SubSystem {
|
||||
this.locale = locale;
|
||||
this.logger = logger;
|
||||
this.errorHandler = errorHandler;
|
||||
nonCriticalExecutor = Executors.newFixedThreadPool(6, new ThreadFactoryBuilder().setNameFormat("Plan Non critical-pool-%d").build());
|
||||
criticalExecutor = Executors.newFixedThreadPool(2, new ThreadFactoryBuilder().setNameFormat("Plan Critical-pool-%d").build());
|
||||
nonCriticalExecutor = createExecutor(6, "Plan Non critical-pool-%d");
|
||||
criticalExecutor = createExecutor(2, "Plan Critical-pool-%d");
|
||||
}
|
||||
|
||||
private ExecutorService createExecutor(int i, String s) {
|
||||
return Executors.newFixedThreadPool(i, new ThreadFactoryBuilder().setNameFormat(s).build());
|
||||
}
|
||||
|
||||
public void submit(Runnable runnable) {
|
||||
@ -126,18 +129,28 @@ public class Processing implements SubSystem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() throws EnableException {
|
||||
public void enable() {
|
||||
if (nonCriticalExecutor.isShutdown()) {
|
||||
throw new EnableException("Non Critical ExecutorService was shut down on enable");
|
||||
nonCriticalExecutor = createExecutor(6, "Plan Non critical-pool-%d");
|
||||
}
|
||||
if (criticalExecutor.isShutdown()) {
|
||||
throw new EnableException("Critical ExecutorService was shut down on enable");
|
||||
criticalExecutor = createExecutor(2, "Plan Critical-pool-%d");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
shutdownNonCriticalExecutor();
|
||||
shutdownCriticalExecutor();
|
||||
ensureShutdown();
|
||||
logger.info(locale.get().getString(PluginLang.DISABLED_PROCESSING_COMPLETE));
|
||||
}
|
||||
|
||||
private void shutdownNonCriticalExecutor() {
|
||||
nonCriticalExecutor.shutdown();
|
||||
}
|
||||
|
||||
private void shutdownCriticalExecutor() {
|
||||
List<Runnable> criticalTasks = criticalExecutor.shutdownNow();
|
||||
logger.info(locale.get().getString(PluginLang.DISABLED_PROCESSING, criticalTasks.size()));
|
||||
for (Runnable runnable : criticalTasks) {
|
||||
@ -148,6 +161,9 @@ public class Processing implements SubSystem {
|
||||
errorHandler.log(L.WARN, this.getClass(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureShutdown() {
|
||||
try {
|
||||
if (!nonCriticalExecutor.isTerminated() && !nonCriticalExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
nonCriticalExecutor.shutdownNow();
|
||||
@ -161,6 +177,5 @@ public class Processing implements SubSystem {
|
||||
criticalExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
logger.info(locale.get().getString(PluginLang.DISABLED_PROCESSING_COMPLETE));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.djrapitops.plan.data.container.Session;
|
||||
import com.djrapitops.plan.data.time.GMTimes;
|
||||
import com.djrapitops.plan.db.Database;
|
||||
import com.djrapitops.plan.db.access.queries.objects.SessionQueries;
|
||||
import com.djrapitops.plan.db.access.transactions.StoreServerInformationTransaction;
|
||||
import com.djrapitops.plan.db.access.transactions.commands.RemoveEverythingTransaction;
|
||||
import com.djrapitops.plan.db.access.transactions.events.PlayerRegisterTransaction;
|
||||
import com.djrapitops.plan.db.access.transactions.events.WorldNameStoreTransaction;
|
||||
import com.djrapitops.plan.system.cache.SessionCache;
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plan.system.info.server.Server;
|
||||
import com.djrapitops.plugin.logging.console.TestPluginLogger;
|
||||
import com.djrapitops.plugin.logging.error.ConsoleErrorLogger;
|
||||
import extension.PrintExtension;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.platform.runner.JUnitPlatform;
|
||||
import org.junit.runner.RunWith;
|
||||
import utilities.TestConstants;
|
||||
import utilities.dagger.DaggerPlanPluginComponent;
|
||||
import utilities.dagger.PlanPluginComponent;
|
||||
import utilities.mocks.PlanPluginMocker;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test ensures that unsaved sessions are saved on server shutdown.
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@RunWith(JUnitPlatform.class)
|
||||
@ExtendWith(PrintExtension.class)
|
||||
public class ShutdownSaveTest {
|
||||
|
||||
private boolean shutdownStatus;
|
||||
private ServerShutdownSave underTest;
|
||||
private Database database;
|
||||
private SessionCache sessionCache;
|
||||
|
||||
@BeforeEach
|
||||
void setupShutdownSaveObject(@TempDir Path temporaryFolder) throws Exception {
|
||||
PlanPluginComponent pluginComponent = DaggerPlanPluginComponent.builder().plan(
|
||||
PlanPluginMocker.setUp()
|
||||
.withDataFolder(temporaryFolder.resolve("ShutdownSaveTest").toFile())
|
||||
.withLogging()
|
||||
.getPlanMock()
|
||||
).build();
|
||||
|
||||
database = pluginComponent.system().getDatabaseSystem().getSqLiteFactory().usingFileCalled("test");
|
||||
database.init();
|
||||
|
||||
sessionCache = pluginComponent.system().getCacheSystem().getSessionCache();
|
||||
|
||||
storeNecessaryInformation();
|
||||
placeSessionToCache();
|
||||
|
||||
DBSystem dbSystemMock = mock(DBSystem.class);
|
||||
when(dbSystemMock.getDatabase()).thenReturn(database);
|
||||
|
||||
underTest = new ServerShutdownSave(dbSystemMock, new ConsoleErrorLogger(new TestPluginLogger())) {
|
||||
@Override
|
||||
protected boolean checkServerShuttingDownStatus() {
|
||||
return shutdownStatus;
|
||||
}
|
||||
};
|
||||
|
||||
shutdownStatus = false;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDownPluginComponent() {
|
||||
database.close();
|
||||
SessionCache.clear();
|
||||
}
|
||||
|
||||
private void storeNecessaryInformation() throws Exception {
|
||||
database.executeTransaction(new RemoveEverythingTransaction());
|
||||
|
||||
UUID serverUUID = TestConstants.SERVER_UUID;
|
||||
UUID playerUUID = TestConstants.PLAYER_ONE_UUID;
|
||||
String worldName = TestConstants.WORLD_ONE_NAME;
|
||||
|
||||
database.executeTransaction(new StoreServerInformationTransaction(new Server(-1, serverUUID, "-", "", 0)));
|
||||
database.executeTransaction(new PlayerRegisterTransaction(playerUUID, () -> 0L, TestConstants.PLAYER_ONE_NAME));
|
||||
database.executeTransaction(new WorldNameStoreTransaction(serverUUID, worldName))
|
||||
.get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionsAreNotSavedOnReload() {
|
||||
shutdownStatus = false;
|
||||
underTest.performSave();
|
||||
|
||||
database.init();
|
||||
assertTrue(database.query(SessionQueries.fetchAllSessions()).isEmpty());
|
||||
database.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionsAreSavedOnServerShutdown() {
|
||||
shutdownStatus = true;
|
||||
underTest.performSave();
|
||||
|
||||
database.init();
|
||||
assertFalse(database.query(SessionQueries.fetchAllSessions()).isEmpty());
|
||||
database.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionsAreSavedOnJVMShutdown() {
|
||||
ShutdownHook shutdownHook = new ShutdownHook(underTest);
|
||||
shutdownHook.run();
|
||||
|
||||
database.init();
|
||||
assertFalse(database.query(SessionQueries.fetchAllSessions()).isEmpty());
|
||||
database.close();
|
||||
}
|
||||
|
||||
private void placeSessionToCache() {
|
||||
UUID serverUUID = TestConstants.SERVER_UUID;
|
||||
UUID playerUUID = TestConstants.PLAYER_ONE_UUID;
|
||||
String worldName = TestConstants.WORLD_ONE_NAME;
|
||||
|
||||
Session session = new Session(playerUUID, serverUUID, 0L, worldName, GMTimes.getGMKeyArray()[0]);
|
||||
|
||||
sessionCache.cacheSession(playerUUID, session);
|
||||
}
|
||||
}
|
43
Plan/common/src/test/java/extension/PrintExtension.java
Normal file
43
Plan/common/src/test/java/extension/PrintExtension.java
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 extension;
|
||||
|
||||
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* JUnit 5 Extension that prints what test is being run before each test.
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public class PrintExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
|
||||
|
||||
@Override
|
||||
public void beforeTestExecution(ExtensionContext context) throws Exception {
|
||||
String testName = context.getTestClass().map(Class::getSimpleName).orElse("?");
|
||||
String testMethodName = context.getTestMethod().map(Method::getName).orElse("?");
|
||||
System.out.println(">> " + testName + " - " + testMethodName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTestExecution(ExtensionContext context) throws Exception {
|
||||
System.out.println();
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ public class TestConstants {
|
||||
|
||||
public static final String PLAYER_ONE_NAME = "Test_Player_one";
|
||||
|
||||
public static final String WORLD_ONE_NAME = "World One";
|
||||
|
||||
public static final int BUKKIT_MAX_PLAYERS = 20;
|
||||
public static final int BUNGEE_MAX_PLAYERS = 100;
|
||||
|
||||
|
@ -22,6 +22,7 @@ import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@ -53,8 +54,10 @@ public class TestResources {
|
||||
}
|
||||
|
||||
private static void copyResourceToFile(File toFile, InputStream testResource) {
|
||||
try (InputStream in = testResource;
|
||||
OutputStream out = new FileOutputStream(toFile)) {
|
||||
try (
|
||||
InputStream in = testResource;
|
||||
OutputStream out = Files.newOutputStream(toFile.toPath())
|
||||
) {
|
||||
copy(in, out);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
@ -62,8 +65,10 @@ public class TestResources {
|
||||
}
|
||||
|
||||
private static void writeResourceToFile(File toFile, String resourcePath) {
|
||||
try (InputStream in = PlanPlugin.class.getResourceAsStream(resourcePath);
|
||||
OutputStream out = new FileOutputStream(toFile)) {
|
||||
try (
|
||||
InputStream in = PlanPlugin.class.getResourceAsStream(resourcePath);
|
||||
OutputStream out = Files.newOutputStream(toFile.toPath())
|
||||
) {
|
||||
if (in == null) {
|
||||
throw new FileNotFoundException("Resource with name '" + resourcePath + "' not found");
|
||||
}
|
||||
@ -83,11 +88,10 @@ public class TestResources {
|
||||
}
|
||||
|
||||
private static void createEmptyFile(File toFile) {
|
||||
String path = toFile.getAbsolutePath();
|
||||
try {
|
||||
toFile.getParentFile().mkdirs();
|
||||
if (!toFile.exists() && !toFile.createNewFile()) {
|
||||
throw new FileNotFoundException("Could not create file: " + path);
|
||||
Files.createDirectories(toFile.toPath().getParent());
|
||||
if (!toFile.exists()) {
|
||||
Files.createFile(toFile.toPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
|
@ -17,8 +17,11 @@
|
||||
package utilities.mocks;
|
||||
|
||||
import com.djrapitops.plan.PlanPlugin;
|
||||
import utilities.TestResources;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
@ -34,42 +37,23 @@ abstract class Mocker {
|
||||
File getFile(String fileName) {
|
||||
// Read the resource from jar to a temporary file
|
||||
File file = new File(new File(planMock.getDataFolder(), "jar"), fileName);
|
||||
try {
|
||||
file.getParentFile().mkdirs();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileNotFoundException("Could not create file: " + fileName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
try (InputStream in = PlanPlugin.class.getResourceAsStream(fileName);
|
||||
OutputStream out = new FileOutputStream(file)) {
|
||||
|
||||
int read;
|
||||
byte[] bytes = new byte[1024];
|
||||
|
||||
while ((read = in.read(bytes)) != -1) {
|
||||
out.write(bytes, 0, read);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
TestResources.copyResourceIntoFile(file, fileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
private void withPluginFile(String fileName) throws FileNotFoundException {
|
||||
private void withPluginFile(String fileName) throws IOException {
|
||||
if (planMock.getDataFolder() == null) {
|
||||
throw new IllegalStateException("withDataFolder needs to be called before setting files");
|
||||
}
|
||||
try {
|
||||
File file = getFile("/" + fileName);
|
||||
doReturn(new FileInputStream(file)).when(planMock).getResource(fileName);
|
||||
doReturn(Files.newInputStream(file.toPath())).when(planMock).getResource(fileName);
|
||||
} catch (NullPointerException e) {
|
||||
System.out.println("File is missing! " + fileName);
|
||||
}
|
||||
}
|
||||
|
||||
void withPluginFiles() throws FileNotFoundException {
|
||||
void withPluginFiles() throws IOException {
|
||||
for (String fileName : new String[]{
|
||||
"bungeeconfig.yml",
|
||||
"config.yml",
|
||||
|
@ -63,6 +63,7 @@ public class PlanSponge extends SpongePlugin implements PlanPlugin {
|
||||
private File dataFolder;
|
||||
private PlanSystem system;
|
||||
private Locale locale;
|
||||
private ServerShutdownSave serverShutdownSave;
|
||||
|
||||
@Listener
|
||||
public void onServerStart(GameStartedServerEvent event) {
|
||||
@ -79,6 +80,7 @@ public class PlanSponge extends SpongePlugin implements PlanPlugin {
|
||||
PlanSpongeComponent component = DaggerPlanSpongeComponent.builder().plan(this).build();
|
||||
try {
|
||||
system = component.system();
|
||||
serverShutdownSave = component.serverShutdownSave();
|
||||
locale = system.getLocaleSystem().getLocale();
|
||||
system.enable();
|
||||
|
||||
@ -112,6 +114,9 @@ public class PlanSponge extends SpongePlugin implements PlanPlugin {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (serverShutdownSave != null) {
|
||||
serverShutdownSave.performSave();
|
||||
}
|
||||
if (system != null) {
|
||||
system.disable();
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ public interface PlanSpongeComponent {
|
||||
|
||||
PlanSystem system();
|
||||
|
||||
ServerShutdownSave serverShutdownSave();
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plugin.logging.error.ErrorHandler;
|
||||
import org.spongepowered.api.GameState;
|
||||
import org.spongepowered.api.event.Listener;
|
||||
import org.spongepowered.api.event.Order;
|
||||
import org.spongepowered.api.event.game.state.GameStoppingServerEvent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* ServerShutdownSave implementation for Sponge
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@Singleton
|
||||
public class SpongeServerShutdownSave extends ServerShutdownSave {
|
||||
|
||||
private boolean shuttingDown = false;
|
||||
|
||||
@Inject
|
||||
public SpongeServerShutdownSave(DBSystem dbSystem, ErrorHandler errorHandler) {
|
||||
super(dbSystem, errorHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkServerShuttingDownStatus() {
|
||||
return shuttingDown;
|
||||
}
|
||||
|
||||
@Listener(order = Order.PRE)
|
||||
public void onServerShutdown(GameStoppingServerEvent event) {
|
||||
GameState state = event.getState();
|
||||
shuttingDown = state == GameState.SERVER_STOPPING
|
||||
|| state == GameState.GAME_STOPPING
|
||||
|| state == GameState.SERVER_STOPPED
|
||||
|| state == GameState.GAME_STOPPED;
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan.modules.sponge;
|
||||
|
||||
import com.djrapitops.plan.ServerShutdownSave;
|
||||
import com.djrapitops.plan.SpongeServerShutdownSave;
|
||||
import com.djrapitops.plan.system.database.DBSystem;
|
||||
import com.djrapitops.plan.system.database.SpongeDBSystem;
|
||||
import com.djrapitops.plan.system.importing.EmptyImportSystem;
|
||||
@ -57,4 +59,7 @@ public interface SpongeSuperClassBindingModule {
|
||||
@Binds
|
||||
ImportSystem bindImportSystem(EmptyImportSystem emptyImportSystem);
|
||||
|
||||
@Binds
|
||||
ServerShutdownSave bindSpongeServerShutdownSave(SpongeServerShutdownSave spongeServerShutdownSave);
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ package com.djrapitops.plan.system.listeners;
|
||||
|
||||
import com.djrapitops.plan.PlanPlugin;
|
||||
import com.djrapitops.plan.PlanSponge;
|
||||
import com.djrapitops.plan.SpongeServerShutdownSave;
|
||||
import com.djrapitops.plan.api.events.PlanSpongeEnableEvent;
|
||||
import com.djrapitops.plan.system.listeners.sponge.*;
|
||||
import org.spongepowered.api.Sponge;
|
||||
@ -36,16 +37,19 @@ public class SpongeListenerSystem extends ListenerSystem {
|
||||
private final SpongeGMChangeListener gmChangeListener;
|
||||
private final SpongePlayerListener playerListener;
|
||||
private final SpongeWorldChangeListener worldChangeListener;
|
||||
private final SpongeServerShutdownSave spongeServerShutdownSave;
|
||||
|
||||
@Inject
|
||||
public SpongeListenerSystem(PlanSponge plugin,
|
||||
public SpongeListenerSystem(
|
||||
PlanSponge plugin,
|
||||
SpongeAFKListener afkListener,
|
||||
SpongeChatListener chatListener,
|
||||
SpongeCommandListener commandListener,
|
||||
SpongeDeathListener deathListener,
|
||||
SpongeGMChangeListener gmChangeListener,
|
||||
SpongePlayerListener playerListener,
|
||||
SpongeWorldChangeListener worldChangeListener
|
||||
SpongeWorldChangeListener worldChangeListener,
|
||||
SpongeServerShutdownSave spongeServerShutdownSave
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
|
||||
@ -56,6 +60,7 @@ public class SpongeListenerSystem extends ListenerSystem {
|
||||
this.gmChangeListener = gmChangeListener;
|
||||
this.playerListener = playerListener;
|
||||
this.worldChangeListener = worldChangeListener;
|
||||
this.spongeServerShutdownSave = spongeServerShutdownSave;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,7 +72,8 @@ public class SpongeListenerSystem extends ListenerSystem {
|
||||
deathListener,
|
||||
playerListener,
|
||||
gmChangeListener,
|
||||
worldChangeListener
|
||||
worldChangeListener,
|
||||
spongeServerShutdownSave
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,10 @@ import org.spongepowered.api.Sponge;
|
||||
import org.spongepowered.api.scheduler.Task;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Singleton
|
||||
public class SpongeTaskSystem extends ServerTaskSystem {
|
||||
|
||||
private final PlanSponge plugin;
|
||||
|
@ -28,6 +28,7 @@ import com.djrapitops.plugin.api.TimeAmount;
|
||||
import com.djrapitops.plugin.task.RunnableFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -35,6 +36,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
@Singleton
|
||||
public class VelocityTaskSystem extends TaskSystem {
|
||||
|
||||
private final PlanVelocity plugin;
|
||||
|
Loading…
Reference in New Issue
Block a user