diff --git a/Plan/common/src/main/java/com/djrapitops/plan/identification/ServerServerInfo.java b/Plan/common/src/main/java/com/djrapitops/plan/identification/ServerServerInfo.java index 05f9604bd..afc7a9b92 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/identification/ServerServerInfo.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/identification/ServerServerInfo.java @@ -19,6 +19,7 @@ package com.djrapitops.plan.identification; import com.djrapitops.plan.delivery.webserver.Addresses; import com.djrapitops.plan.exceptions.EnableException; import com.djrapitops.plan.identification.properties.ServerProperties; +import com.djrapitops.plan.identification.storage.AtomicServerLoader; import com.djrapitops.plan.identification.storage.ServerDBLoader; import com.djrapitops.plan.identification.storage.ServerFileLoader; import com.djrapitops.plan.identification.storage.ServerLoader; @@ -70,8 +71,8 @@ public class ServerServerInfo extends ServerInfo { ) { super(serverProperties); this.currentVersion = currentVersion; - this.fromFile = fromFile; - this.fromDatabase = fromDatabase; + this.fromFile = new AtomicServerLoader(fromFile); + this.fromDatabase = new AtomicServerLoader(fromDatabase); this.processing = processing; this.addresses = addresses; this.config = config; diff --git a/Plan/common/src/main/java/com/djrapitops/plan/identification/storage/AtomicServerLoader.java b/Plan/common/src/main/java/com/djrapitops/plan/identification/storage/AtomicServerLoader.java new file mode 100644 index 000000000..ba3c430a1 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/identification/storage/AtomicServerLoader.java @@ -0,0 +1,54 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.identification.storage; + +import com.djrapitops.plan.identification.Server; +import com.djrapitops.plan.identification.ServerUUID; + +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; + +public class AtomicServerLoader implements ServerLoader { + + private final ReentrantLock reentrantLock; + private final ServerLoader original; + + public AtomicServerLoader(ServerLoader original) { + this.original = original; + this.reentrantLock = new ReentrantLock(); + } + + @Override + public Optional load(ServerUUID serverUUID) { + try { + reentrantLock.lock(); + return original.load(serverUUID); + } finally { + reentrantLock.unlock(); + } + } + + @Override + public void save(Server information) { + try { + reentrantLock.lock(); + original.save(information); + } finally { + reentrantLock.unlock(); + } + } +} diff --git a/Plan/common/src/test/java/com/djrapitops/plan/identification/storage/ServerFileLoaderTest.java b/Plan/common/src/test/java/com/djrapitops/plan/identification/storage/ServerFileLoaderTest.java new file mode 100644 index 000000000..ea2bf2b65 --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/identification/storage/ServerFileLoaderTest.java @@ -0,0 +1,82 @@ +package com.djrapitops.plan.identification.storage; + +import com.djrapitops.plan.PlanSystem; +import com.djrapitops.plan.identification.Server; +import com.djrapitops.plan.identification.ServerUUID; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import utilities.TestConstants; +import utilities.mocks.PluginMockComponent; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServerFileLoaderTest { + static PlanSystem system; + static ServerFileLoader underTest; + private static ServerUUID serverUUID; + + @BeforeAll + static void setUp(@TempDir Path tempDir) throws Exception { + PluginMockComponent mockComponent = new PluginMockComponent(tempDir); + system = mockComponent.getPlanSystem(); + system.enable(); + } + + @AfterAll + static void tearDown() { + if (system != null) system.disable(); + } + + @BeforeEach + void setUpEach() { + underTest = new ServerFileLoader(TestConstants.VERSION, system.getPlanFiles(), system.getConfigSystem().getConfig()); + Optional loaded = underTest.load(null); + assertTrue(loaded.isPresent()); + + if (serverUUID == null) { + serverUUID = loaded.get().getUuid(); + } + } + + @Test + void runParallelLoadsAndSaves() throws InterruptedException { + ExecutorService executorService = new ScheduledThreadPoolExecutor(6); + + AtomicInteger runs = new AtomicInteger(1); + int expected = 10000; + AtomicInteger fails = new AtomicInteger(0); + try { + for (int i = 0; i < expected; i++) { + executorService.submit(() -> { + Optional load = underTest.load(null); + if (load.isPresent()) { + underTest.save(load.get()); + } else { + System.out.println("Failure " + fails.incrementAndGet()); + } + }); + } + Awaitility.await() + .atMost(2, TimeUnit.MINUTES) + .until(() -> runs.get() >= expected); + } finally { + executorService.shutdown(); + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } + assertEquals(0, fails.get()); + } +} \ No newline at end of file