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