[#769, #928] Session save on server shutdown (#927)

* 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:
Risto Lahtela 2019-02-24 12:28:58 +02:00 committed by GitHub
parent 96564c90be
commit 16e6ef1dc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 561 additions and 115 deletions

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -53,6 +53,8 @@ public interface PlanBukkitComponent {
PlanSystem system();
ServerShutdownSave serverShutdownSave();
@Component.Builder
interface Builder {

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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}"),

View File

@ -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));
}
}

View File

@ -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);
}
}

View 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();
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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",

View File

@ -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();
}

View File

@ -53,6 +53,8 @@ public interface PlanSpongeComponent {
PlanSystem system();
ServerShutdownSave serverShutdownSave();
@Component.Builder
interface Builder {

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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,
SpongeAFKListener afkListener,
SpongeChatListener chatListener,
SpongeCommandListener commandListener,
SpongeDeathListener deathListener,
SpongeGMChangeListener gmChangeListener,
SpongePlayerListener playerListener,
SpongeWorldChangeListener worldChangeListener
public SpongeListenerSystem(
PlanSponge plugin,
SpongeAFKListener afkListener,
SpongeChatListener chatListener,
SpongeCommandListener commandListener,
SpongeDeathListener deathListener,
SpongeGMChangeListener gmChangeListener,
SpongePlayerListener playerListener,
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
);
}

View File

@ -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;

View File

@ -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;