From 92868fe50b4735dfad6ed8f37403ee021da7f452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 30 Aug 2025 22:10:38 +0200 Subject: [PATCH 1/8] Add database for users, add batch fetching, update tests --- .../kotlin/eternalcode-java-test.gradle.kts | 3 + .../implementation/PluginConfiguration.java | 8 ++ .../database/AbstractRepositoryOrmLite.java | 4 + .../core/database/DatabaseConfig.java | 2 +- .../core/database/DatabaseDriverType.java | 4 +- .../core/database/DatabaseManager.java | 1 + .../java/com/eternalcode/core/user/User.java | 2 +- .../eternalcode/core/user/UserManager.java | 71 ++++++++++-- .../core/user/database/UserRepository.java | 21 ++++ .../user/database/UserRepositoryConfig.java | 24 ++++ .../user/database/UserRepositoryOrmLite.java | 84 ++++++++++++++ .../user/database/UserRepositorySettings.java | 8 ++ .../core/user/database/UserTable.java | 32 ++++++ .../core/test/MockUserRepository.java | 39 +++++++ .../core/test/MockUserRepositorySettings.java | 16 +++ .../com/eternalcode/core/user/BatchTest.java | 107 ++++++++++++++++++ .../core/user/PrepareUserControllerTest.java | 7 +- .../core/user/UserManagerTest.java | 15 ++- .../core/util/IntegrationTestSpec.java | 13 +++ .../eternalcode/core/util/TestScheduler.java | 92 +++++++++++++++ 20 files changed, 534 insertions(+), 19 deletions(-) create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java diff --git a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts index 96e7d8cd9..73ae04728 100644 --- a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts +++ b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts @@ -10,6 +10,9 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.testcontainers:junit-jupiter:1.21.3") + testImplementation("org.testcontainers:mysql:1.21.3") + testImplementation("mysql:mysql-connector-java:8.0.33") testImplementation("org.mockito:mockito-core:${Versions.MOCKITO_CORE}") testImplementation("net.kyori:adventure-platform-facet:${Versions.ADVENTURE_PLATFORM}") testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index 5370be646..47bfcce51 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -41,6 +41,8 @@ import com.eternalcode.core.injector.annotations.component.ConfigurationFile; import com.eternalcode.core.translation.TranslationConfig; import com.eternalcode.core.translation.TranslationSettings; +import com.eternalcode.core.user.database.UserRepositoryConfig; +import com.eternalcode.core.user.database.UserRepositorySettings; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; @@ -79,6 +81,12 @@ public class PluginConfiguration extends AbstractConfigurationFile { @Comment("# Settings responsible for the database connection") DatabaseConfig database = new DatabaseConfig(); + @Bean(proxied = UserRepositorySettings.class) + @Comment("") + @Comment("# User Repository Configuration") + @Comment("# Settings for managing user data storage and retrieval") + UserRepositoryConfig userRepository = new UserRepositoryConfig(); + @Bean(proxied = SpawnJoinSettings.class) @Comment("") @Comment("# Spawn & Join Configuration") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java index 262249442..1914a7e52 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java @@ -50,6 +50,10 @@ protected CompletableFuture> selectAll(Class type) { return this.action(type, Dao::queryForAll); } + protected CompletableFuture> selectBatch(Class type, int offset, int limit) { + return this.action(type, dao -> dao.queryBuilder().offset((long) offset).limit((long) limit).query()); + } + protected CompletableFuture action( Class type, ThrowingFunction, R, SQLException> action diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java index 869ef687c..eb41b7e22 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java @@ -13,7 +13,7 @@ public class DatabaseConfig extends OkaeriConfig implements DatabaseSettings { @Comment({"Type of the database driver (e.g., SQLITE, H2, MYSQL, MARIADB, POSTGRESQL).", "Determines the " + "database type " + "to be used."}) - public DatabaseDriverType databaseType = DatabaseDriverType.SQLITE; + public DatabaseDriverType databaseType = DatabaseDriverType.MYSQL; @Comment({"Hostname of the database server.", "For local databases, this is usually 'localhost'."}) public String hostname = "localhost"; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java index c40f84ebd..00407fb3d 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java @@ -8,7 +8,9 @@ public enum DatabaseDriverType { MARIADB(MARIADB_DRIVER, MARIADB_JDBC_URL), POSTGRESQL(POSTGRESQL_DRIVER, POSTGRESQL_JDBC_URL), H2(H2_DRIVER, H2_JDBC_URL), - SQLITE(SQLITE_DRIVER, SQLITE_JDBC_URL); + SQLITE(SQLITE_DRIVER, SQLITE_JDBC_URL), + + H2_TEST(H2_DRIVER, "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL"); private final String driver; private final String urlFormat; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java index badb85de0..195cb801f 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java @@ -67,6 +67,7 @@ public void connect() { settings.database(), String.valueOf(settings.ssl()) ); + case H2_TEST -> type.formatUrl(); }; this.dataSource.setJdbcUrl(jdbcUrl); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 0536b877a..12d6c45a4 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -12,7 +12,7 @@ public class User implements Viewer { private final String name; private final UUID uuid; - User(UUID uuid, String name) { + public User(UUID uuid, String name) { this.name = name; this.uuid = uuid; } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index e6bcfc0f2..4e0e5a2d1 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -1,45 +1,65 @@ package com.eternalcode.core.user; +import com.eternalcode.commons.algorithm.BatchProcessor; +import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; +import com.eternalcode.core.user.database.UserRepository; +import com.eternalcode.core.user.database.UserRepositorySettings; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import java.util.Collection; import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; @Service public class UserManager { - private final Map usersByUUID = new ConcurrentHashMap<>(); - private final Map usersByName = new ConcurrentHashMap<>(); + private final Cache usersByUUID; + private final Cache usersByName; + + private final UserRepository userRepository; + private final UserRepositorySettings userRepositorySettings; + + @Inject + public UserManager(UserRepository userRepository, UserRepositorySettings userRepositorySettings) { + this.userRepositorySettings = userRepositorySettings; + this.usersByUUID = Caffeine.newBuilder().build(); + this.usersByName = Caffeine.newBuilder().build(); + + this.userRepository = userRepository; + + fetchUsers(); + } public Optional getUser(UUID uuid) { - return Optional.ofNullable(this.usersByUUID.get(uuid)); + return Optional.ofNullable(this.usersByUUID.getIfPresent(uuid)); } public Optional getUser(String name) { - return Optional.ofNullable(this.usersByName.get(name)); + return Optional.ofNullable(this.usersByName.getIfPresent(name)); } public User getOrCreate(UUID uuid, String name) { - User userByUUID = this.usersByUUID.get(uuid); + User userByUUID = this.usersByUUID.getIfPresent(uuid); if (userByUUID != null) { return userByUUID; } - User userByName = this.usersByName.get(name); + User userByName = this.usersByName.getIfPresent(name); if (userByName != null) { return userByName; } + this.userRepository.saveUser(new User(uuid, name)); return this.create(uuid, name); } public User create(UUID uuid, String name) { - if (this.usersByUUID.containsKey(uuid) || this.usersByName.containsKey(name)) { + if (this.usersByName.getIfPresent(name) != null || this.usersByUUID.getIfPresent(uuid) != null) { throw new IllegalStateException("User already exists"); } @@ -47,10 +67,41 @@ public User create(UUID uuid, String name) { this.usersByUUID.put(uuid, user); this.usersByName.put(name, user); + this.userRepository.saveUser(user); return user; } public Collection getUsers() { - return Collections.unmodifiableCollection(this.usersByUUID.values()); + return Collections.unmodifiableCollection(this.usersByUUID.asMap().values()); + } + + private void fetchUsers() { + if (this.userRepositorySettings.batchDatabaseFetchSize() <= 0) { + throw new IllegalArgumentException("Value for batchDatabaseFetchSize must be greater than 0!"); + } + + Consumer> batchSave = users -> + { + BatchProcessor batchProcessor = new BatchProcessor<>(users, this.userRepositorySettings.batchDatabaseFetchSize()); + + do { + batchProcessor.processNext(user -> { + usersByName.put(user.getName(), user); + usersByUUID.put(user.getUniqueId(), user); + }); + + } while (!batchProcessor.isComplete()); + }; + + if (this.userRepositorySettings.useBatchDatabaseFetching()) { + this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()) + .thenAccept(batchSave); + } + else { + + this.userRepository.fetchAllUsers() + .thenAccept(batchSave); + + } } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java new file mode 100644 index 000000000..efda48404 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -0,0 +1,21 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.core.user.User; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface UserRepository { + + CompletableFuture getUser(UUID uniqueId); + + CompletableFuture saveUser(User player); + + CompletableFuture updateUser(UUID uniqueId, User player); + + CompletableFuture deleteUser(UUID uniqueId); + + CompletableFuture> fetchAllUsers(); + + CompletableFuture> fetchUsersBatch(int batchSize); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java new file mode 100644 index 000000000..311c9b1e1 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java @@ -0,0 +1,24 @@ +package com.eternalcode.core.user.database; + +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +public class UserRepositoryConfig extends OkaeriConfig implements UserRepositorySettings { + + @Comment({ + "# Should plugin use batches to fetch users from the database?", + "# We suggest turning this setting to TRUE for servers with more than 10k users", + "# Set this to false if you are using SQLITE or H2 database (local databases)" + }) + public boolean useBatchDatabaseFetching = false; + + @Comment({ + "# Size of batches querried to the database", + "# Value must be greater than 0!" + }) + public int batchDatabaseFetchSize = 1000; +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java new file mode 100644 index 000000000..953d8a6c4 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -0,0 +1,84 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.core.database.AbstractRepositoryOrmLite; +import com.eternalcode.core.database.DatabaseManager; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Repository; +import com.eternalcode.core.user.User; +import com.j256.ormlite.table.TableUtils; +import java.sql.SQLException; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Repository +public class UserRepositoryOrmLite extends AbstractRepositoryOrmLite implements UserRepository { + + @Inject + public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { + super(databaseManager, scheduler); + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), UserTable.class); + } + + @Override + public CompletableFuture getUser(UUID uniqueId) { + return this.selectSafe(UserTable.class, uniqueId) + .thenApply(optional -> optional.map(userTable -> userTable.toUser()).orElseGet(null)); + } + + @Override + public CompletableFuture> fetchAllUsers() { + return this.selectAll(UserTable.class) + .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()); + } + + @Override + public CompletableFuture> fetchUsersBatch(int batchSize) { + return CompletableFuture.supplyAsync(() -> { + try { + var dao = this.databaseManager.getDao(UserTable.class); + var users = new java.util.ArrayList(); + + int offset = 0; + while (true) { + var queryBuilder = dao.queryBuilder(); + queryBuilder.limit((long) batchSize); + queryBuilder.offset((long) offset); + + var batch = dao.query(queryBuilder.prepare()); + + if (batch.isEmpty()) { + break; + } + + batch.stream() + .map(UserTable::toUser) + .forEach(users::add); + + offset += batchSize; + } + + return users; + } catch (Exception exception) { + throw new RuntimeException("Failed to fetch users in batches", exception); + } + }); + } + + @Override + public CompletableFuture saveUser(User user) { + return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> null); + } + + @Override + public CompletableFuture updateUser(UUID uniqueId, User user) { + return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> user); + } + + @Override + public CompletableFuture deleteUser(UUID uniqueId) { + return this.deleteById(UserTable.class, uniqueId).thenApply(v -> null); + } + +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java new file mode 100644 index 000000000..e7ad7fa59 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java @@ -0,0 +1,8 @@ +package com.eternalcode.core.user.database; + +public interface UserRepositorySettings { + + boolean useBatchDatabaseFetching(); + + int batchDatabaseFetchSize(); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java new file mode 100644 index 000000000..f872901fc --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -0,0 +1,32 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.core.user.User; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import java.util.UUID; +import lombok.Data; + +@DatabaseTable(tableName = "eternal_core_users") +public class UserTable { + + @DatabaseField(columnName = "id", id = true) + private UUID uniqueId; + + @DatabaseField(columnName = "name") + private String name; + + UserTable() {} + + UserTable(UUID uniqueId, String name) { + this.uniqueId = uniqueId; + this.name = name; + } + + public User toUser() { + return new User(this.uniqueId, this.name); + } + + public static UserTable from(User user) { + return new UserTable(user.getUniqueId(), user.getName()); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java new file mode 100644 index 000000000..b0de6f80f --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -0,0 +1,39 @@ +package com.eternalcode.core.test; + +import com.eternalcode.core.user.User; +import com.eternalcode.core.user.database.UserRepository; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class MockUserRepository implements UserRepository { + @Override + public CompletableFuture getUser(UUID uniqueId) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture saveUser(User player) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture updateUser(UUID uniqueId, User player) { + return CompletableFuture.completedFuture(player); + } + + @Override + public CompletableFuture deleteUser(UUID uniqueId) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> fetchAllUsers() { + return CompletableFuture.completedFuture(java.util.List.of()); + } + + @Override + public CompletableFuture> fetchUsersBatch(int batchSize) { + return CompletableFuture.completedFuture(java.util.List.of()); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java new file mode 100644 index 000000000..bf2c244ed --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java @@ -0,0 +1,16 @@ +package com.eternalcode.core.test; + +import com.eternalcode.core.user.database.UserRepositorySettings; + +public class MockUserRepositorySettings implements UserRepositorySettings { + + @Override + public boolean useBatchDatabaseFetching() { + return false; + } + + @Override + public int batchDatabaseFetchSize() { + return 100; + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java new file mode 100644 index 000000000..90caedb8b --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java @@ -0,0 +1,107 @@ +package com.eternalcode.core.user; + +import com.eternalcode.core.database.DatabaseConfig; +import com.eternalcode.core.database.DatabaseDriverType; +import com.eternalcode.core.database.DatabaseManager; +import com.eternalcode.core.database.DatabaseSettings; +import com.eternalcode.core.user.database.UserRepository; +import com.eternalcode.core.user.database.UserRepositoryConfig; +import com.eternalcode.core.user.database.UserRepositoryOrmLite; +import com.eternalcode.core.user.database.UserRepositorySettings; +import com.eternalcode.core.util.IntegrationTestSpec; +import com.eternalcode.core.util.TestScheduler; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +class BatchTest extends IntegrationTestSpec { + + @Container + private static final MySQLContainer container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")) + .withUsername("test") + .withPassword("test") + .withDatabaseName("testdb"); + + private final TestScheduler testScheduler = new TestScheduler(); + + private DatabaseManager databaseManager; + + @Test + void testWithMySQL(@TempDir Path tempDir) throws SQLException { + DatabaseConfig config = new DatabaseConfig(); + config.username = container.getUsername(); + config.password = container.getPassword(); + config.database = container.getDatabaseName(); + + config.hostname = container.getHost(); + config.port = container.getFirstMappedPort(); + + databaseManager = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); + databaseManager.connect(); + + UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, this.testScheduler); + UserManager userManager = new UserManager(userRepository, new UserRepositoryConfig()); + + Assertions.assertEquals(0, userManager.getUsers().size()); + + UUID randomUUID = UUID.randomUUID(); + userManager.getOrCreate(randomUUID, "test1"); + + Assertions.assertEquals(1, userManager.getUsers().size()); + + userRepository.getUser(randomUUID).thenAccept(user -> { + Assertions.assertNotNull(user); + Assertions.assertEquals("test1", user.getName()); + }); + + databaseManager.close(); + } + + @Test + void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { + DatabaseConfig config = new DatabaseConfig(); + config.databaseType = DatabaseDriverType.H2_TEST; + config.username = "sa"; + config.password = ""; + + // ✅ Use H2 in-memory database + config.hostname = null; // not used + config.port = 0; // not used + config.database = "eternalcode"; // any name works + + DatabaseManager db = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); + db.connect(); + + UserRepository userRepo = new UserRepositoryOrmLite(db, new TestScheduler()); + + // Insert 5000 users + for (int i = 0; i < 50000; i++) { + userRepo.saveUser(new User(UUID.randomUUID(), "user" + i)).join(); + } + + long start = System.nanoTime(); + var allUsers = userRepo.fetchAllUsers().join(); + long allFetchTime = System.nanoTime() - start; + + start = System.nanoTime(); + var batchedUsers = userRepo.fetchUsersBatch(500).join(); + long batchFetchTime = System.nanoTime() - start; + + System.out.printf("All fetch: %d ms, Batched fetch: %d ms%n", + allFetchTime / 1_000_000, batchFetchTime / 1_000_000); + + Assertions.assertEquals(allUsers.size(), batchedUsers.size()); + } + + +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java index 2b0be982a..3aca45fa2 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java @@ -1,6 +1,8 @@ package com.eternalcode.core.user; import com.eternalcode.core.test.MockServer; +import com.eternalcode.core.test.MockUserRepository; +import com.eternalcode.core.test.MockUserRepositorySettings; import org.bukkit.entity.Player; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -24,8 +26,11 @@ class PrepareUserControllerTest { @BeforeEach void setUp() { + MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); + MockUserRepository mockUserRepository = new MockUserRepository(); + this.mockServer = new MockServer(); - this.userManager = new UserManager(); + this.userManager = new UserManager(mockUserRepository, mockUserRepositorySettings); PrepareUserController controller = new PrepareUserController(this.userManager, this.mockServer.getServer()); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java index 1c4c3fcfa..57da100ca 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java @@ -1,5 +1,7 @@ package com.eternalcode.core.user; +import com.eternalcode.core.test.MockUserRepository; +import com.eternalcode.core.test.MockUserRepositorySettings; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -10,9 +12,12 @@ class UserManagerTest { + private final MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); + private final MockUserRepository mockUserRepository = new MockUserRepository(); + @Test void testUsersCreate() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); assertEquals(0, manager.getUsers().size()); @@ -27,7 +32,7 @@ void testUsersCreate() { @Test void testCreateSameUser() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); manager.create(UUID.randomUUID(), "Piotr"); assertThrows(IllegalStateException.class, () -> manager.create(UUID.randomUUID(), "Piotr")); @@ -39,7 +44,7 @@ void testCreateSameUser() { @Test void testGetUsers() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); assertEquals(0, manager.getUsers().size()); @@ -52,7 +57,7 @@ void testGetUsers() { @Test void testGetUser() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); assertEquals(0, manager.getUsers().size()); @@ -65,7 +70,7 @@ void testGetUser() { @Test void testGetOrCreate() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); UUID uuid = UUID.randomUUID(); User user = manager.getOrCreate(uuid, "Michał"); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java new file mode 100644 index 000000000..5f08834eb --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java @@ -0,0 +1,13 @@ +package com.eternalcode.core.util; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class IntegrationTestSpec { + + public T await(CompletableFuture future) { + return future + .orTimeout(5, TimeUnit.SECONDS) + .join(); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java new file mode 100644 index 000000000..485b10386 --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java @@ -0,0 +1,92 @@ +package com.eternalcode.core.util; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.commons.scheduler.Task; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class TestScheduler implements Scheduler { + + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(8); + + public void shutdown() { + this.executorService.shutdown(); + } + + @Override + public Task run(Runnable runnable) { + Future future = this.executorService.submit(runnable); + return new TestTask(future, false); + } + + @Override + public Task runAsync(Runnable runnable) { + Future future = CompletableFuture.runAsync(runnable, this.executorService); + return new TestTask(future, false); + } + + @Override + public Task runLater(Runnable runnable, Duration duration) { + ScheduledFuture future = this.executorService.schedule(runnable, duration.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, false); + } + + @Override + public Task runLaterAsync(Runnable runnable, Duration duration) { + ScheduledFuture future = this.executorService.schedule(() -> CompletableFuture.runAsync(runnable, + this.executorService), duration.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, false); + } + + @Override + public Task timer(Runnable runnable, Duration initialDelay, Duration period) { + ScheduledFuture future = this.executorService.scheduleAtFixedRate(runnable, initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, true); + } + + @Override + public Task timerAsync(Runnable runnable, Duration initialDelay, Duration period) { + ScheduledFuture future = this.executorService.scheduleAtFixedRate(() -> CompletableFuture.runAsync(runnable, + this.executorService), initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, true); + } + + @Override + public CompletableFuture complete(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, this.executorService); + } + + @Override + public CompletableFuture completeAsync(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, this.executorService); + } + + private record TestTask(Future future, boolean isRepeating) implements Task { + + @Override + public void cancel() { + this.future.cancel(false); + } + + @Override + public boolean isCanceled() { + return this.future.isCancelled(); + } + + @Override + public boolean isAsync() { + return this.future instanceof CompletableFuture || this.future instanceof ScheduledFuture; + } + + @Override + public boolean isRunning() { + return !this.future.isDone(); + } + } +} From 1a7dd1819656b8871d686b1d6927ae90f3983444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 30 Aug 2025 22:14:04 +0200 Subject: [PATCH 2/8] Codestyle fixes --- .../com/eternalcode/core/user/database/UserRepository.java | 3 ++- .../java/com/eternalcode/core/user/database/UserTable.java | 1 - .../src/test/java/com/eternalcode/core/user/BatchTest.java | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index efda48404..795e3b3fc 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -4,10 +4,11 @@ import java.util.Collection; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.Nullable; public interface UserRepository { - CompletableFuture getUser(UUID uniqueId); + @Nullable CompletableFuture getUser(UUID uniqueId); CompletableFuture saveUser(User player); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index f872901fc..a79f7d385 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -4,7 +4,6 @@ import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import java.util.UUID; -import lombok.Data; @DatabaseTable(tableName = "eternal_core_users") public class UserTable { diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java index 90caedb8b..0d7f4995e 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java @@ -3,17 +3,14 @@ import com.eternalcode.core.database.DatabaseConfig; import com.eternalcode.core.database.DatabaseDriverType; import com.eternalcode.core.database.DatabaseManager; -import com.eternalcode.core.database.DatabaseSettings; import com.eternalcode.core.user.database.UserRepository; import com.eternalcode.core.user.database.UserRepositoryConfig; import com.eternalcode.core.user.database.UserRepositoryOrmLite; -import com.eternalcode.core.user.database.UserRepositorySettings; import com.eternalcode.core.util.IntegrationTestSpec; import com.eternalcode.core.util.TestScheduler; import java.nio.file.Path; import java.sql.SQLException; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; From 3c6ad2a2f648ff3189bf7653b3ba876bbeeabcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 30 Aug 2025 23:48:27 +0200 Subject: [PATCH 3/8] Resolve gemini review --- buildSrc/src/main/kotlin/Versions.kt | 2 + .../kotlin/eternalcode-java-test.gradle.kts | 6 +-- .../eternalcode/core/user/UserManager.java | 46 ++++++++----------- .../core/user/database/UserRepository.java | 5 +- .../user/database/UserRepositoryOrmLite.java | 22 ++++----- .../core/test/MockUserRepository.java | 5 +- ...BatchTest.java => UserBatchFetchTest.java} | 4 +- 7 files changed, 44 insertions(+), 46 deletions(-) rename eternalcore-core/src/test/java/com/eternalcode/core/user/{BatchTest.java => UserBatchFetchTest.java} (96%) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 4f3c23e32..b0d6a327a 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -48,5 +48,7 @@ object Versions { // tests const val JUNIT_BOM = "5.13.4" const val MOCKITO_CORE = "5.19.0" + const val TEST_CONTAINERS = "1.21.3" + const val MYSQL_CONNECTOR = "8.0.33" } diff --git a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts index 73ae04728..0e7901ed6 100644 --- a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts +++ b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts @@ -10,9 +10,9 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("org.testcontainers:junit-jupiter:1.21.3") - testImplementation("org.testcontainers:mysql:1.21.3") - testImplementation("mysql:mysql-connector-java:8.0.33") + testImplementation("org.testcontainers:junit-jupiter:${Versions.TEST_CONTAINERS}") + testImplementation("org.testcontainers:mysql:${Versions.TEST_CONTAINERS}") + testImplementation("mysql:mysql-connector-java:${Versions.MYSQL_CONNECTOR}") testImplementation("org.mockito:mockito-core:${Versions.MOCKITO_CORE}") testImplementation("net.kyori:adventure-platform-facet:${Versions.ADVENTURE_PLATFORM}") testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 4e0e5a2d1..cfd37ed67 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -1,6 +1,5 @@ package com.eternalcode.core.user; -import com.eternalcode.commons.algorithm.BatchProcessor; import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; @@ -11,6 +10,7 @@ import java.util.Collections; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @Service @@ -18,6 +18,7 @@ public class UserManager { private final Cache usersByUUID; private final Cache usersByName; + private boolean fetched = false; private final UserRepository userRepository; private final UserRepositorySettings userRepositorySettings; @@ -30,7 +31,7 @@ public UserManager(UserRepository userRepository, UserRepositorySettings userRep this.userRepository = userRepository; - fetchUsers(); + fetchUsers().thenRun(() -> this.fetched = true); } public Optional getUser(UUID uuid) { @@ -38,34 +39,37 @@ public Optional getUser(UUID uuid) { } public Optional getUser(String name) { - return Optional.ofNullable(this.usersByName.getIfPresent(name)); + return Optional.ofNullable(this.usersByName.getIfPresent(name.toLowerCase())); } public User getOrCreate(UUID uuid, String name) { + if (!this.fetched) { + throw new IllegalStateException("Users have not been fetched from the database yet!"); + } + User userByUUID = this.usersByUUID.getIfPresent(uuid); if (userByUUID != null) { return userByUUID; } - User userByName = this.usersByName.getIfPresent(name); + User userByName = this.usersByName.getIfPresent(name.toLowerCase()); if (userByName != null) { return userByName; } - this.userRepository.saveUser(new User(uuid, name)); return this.create(uuid, name); } public User create(UUID uuid, String name) { - if (this.usersByName.getIfPresent(name) != null || this.usersByUUID.getIfPresent(uuid) != null) { + if (this.usersByName.getIfPresent(name.toLowerCase()) != null || this.usersByUUID.getIfPresent(uuid) != null) { throw new IllegalStateException("User already exists"); } User user = new User(uuid, name); this.usersByUUID.put(uuid, user); - this.usersByName.put(name, user); + this.usersByName.put(name.toLowerCase(), user); this.userRepository.saveUser(user); return user; @@ -75,33 +79,23 @@ public Collection getUsers() { return Collections.unmodifiableCollection(this.usersByUUID.asMap().values()); } - private void fetchUsers() { + private CompletableFuture fetchUsers() { if (this.userRepositorySettings.batchDatabaseFetchSize() <= 0) { throw new IllegalArgumentException("Value for batchDatabaseFetchSize must be greater than 0!"); } - Consumer> batchSave = users -> - { - BatchProcessor batchProcessor = new BatchProcessor<>(users, this.userRepositorySettings.batchDatabaseFetchSize()); - - do { - batchProcessor.processNext(user -> { - usersByName.put(user.getName(), user); - usersByUUID.put(user.getUniqueId(), user); - }); - - } while (!batchProcessor.isComplete()); - }; + Consumer> batchSave = users -> users.forEach(user -> { + this.usersByName.put(user.getName(), user); + this.usersByUUID.put(user.getUniqueId(), user); + }); if (this.userRepositorySettings.useBatchDatabaseFetching()) { - this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()) - .thenAccept(batchSave); + this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()).thenAccept(batchSave); } else { - - this.userRepository.fetchAllUsers() - .thenAccept(batchSave); - + this.userRepository.fetchAllUsers().thenAccept(batchSave); } + + return CompletableFuture.completedFuture(null); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 795e3b3fc..3df08b53c 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -2,17 +2,18 @@ import com.eternalcode.core.user.User; import java.util.Collection; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.jetbrains.annotations.Nullable; public interface UserRepository { - @Nullable CompletableFuture getUser(UUID uniqueId); + CompletableFuture> getUser(UUID uniqueId); CompletableFuture saveUser(User player); - CompletableFuture updateUser(UUID uniqueId, User player); + CompletableFuture updateUser(User player); CompletableFuture deleteUser(UUID uniqueId); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java index 953d8a6c4..6d5d7f33c 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -8,7 +8,10 @@ import com.eternalcode.core.user.User; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -22,31 +25,28 @@ public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler schedule } @Override - public CompletableFuture getUser(UUID uniqueId) { + public CompletableFuture> getUser(UUID uniqueId) { return this.selectSafe(UserTable.class, uniqueId) - .thenApply(optional -> optional.map(userTable -> userTable.toUser()).orElseGet(null)); + .thenApply(optional -> optional.map(UserTable::toUser)); } @Override public CompletableFuture> fetchAllUsers() { return this.selectAll(UserTable.class) - .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()); + .thenApply(userTables -> userTables.stream() + .map(UserTable::toUser) + .toList()); } @Override public CompletableFuture> fetchUsersBatch(int batchSize) { return CompletableFuture.supplyAsync(() -> { try { - var dao = this.databaseManager.getDao(UserTable.class); - var users = new java.util.ArrayList(); + var users = new ArrayList(); int offset = 0; while (true) { - var queryBuilder = dao.queryBuilder(); - queryBuilder.limit((long) batchSize); - queryBuilder.offset((long) offset); - - var batch = dao.query(queryBuilder.prepare()); + List batch = this.selectBatch(UserTable.class, offset, batchSize).join(); if (batch.isEmpty()) { break; @@ -72,7 +72,7 @@ public CompletableFuture saveUser(User user) { } @Override - public CompletableFuture updateUser(UUID uniqueId, User user) { + public CompletableFuture updateUser(User user) { return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> user); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java index b0de6f80f..f5f3bb26d 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -3,12 +3,13 @@ import com.eternalcode.core.user.User; import com.eternalcode.core.user.database.UserRepository; import java.util.Collection; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class MockUserRepository implements UserRepository { @Override - public CompletableFuture getUser(UUID uniqueId) { + public CompletableFuture> getUser(UUID uniqueId) { return CompletableFuture.completedFuture(null); } @@ -18,7 +19,7 @@ public CompletableFuture saveUser(User player) { } @Override - public CompletableFuture updateUser(UUID uniqueId, User player) { + public CompletableFuture updateUser(User player) { return CompletableFuture.completedFuture(player); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java similarity index 96% rename from eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java rename to eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java index 0d7f4995e..a923b8240 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java @@ -21,7 +21,7 @@ import org.testcontainers.utility.DockerImageName; @Testcontainers -class BatchTest extends IntegrationTestSpec { +class UserBatchFetchTest extends IntegrationTestSpec { @Container private static final MySQLContainer container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")) @@ -58,7 +58,7 @@ void testWithMySQL(@TempDir Path tempDir) throws SQLException { userRepository.getUser(randomUUID).thenAccept(user -> { Assertions.assertNotNull(user); - Assertions.assertEquals("test1", user.getName()); + Assertions.assertEquals("test1", user.get().getName()); }); databaseManager.close(); From 7d3ee9e0f5426b2b532c037a1df7e89f9666b6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Mon, 1 Sep 2025 21:58:40 +0200 Subject: [PATCH 4/8] Resolve @sadcenter and @Jakubk15 reviews --- .../core/user/UserBatchFetchTest.java | 22 ++++++++++--------- .../core/util/IntegrationTestSpec.java | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java index a923b8240..0f04f7fd6 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java @@ -32,6 +32,7 @@ class UserBatchFetchTest extends IntegrationTestSpec { private final TestScheduler testScheduler = new TestScheduler(); private DatabaseManager databaseManager; + private final Logger logger = Logger.getLogger("test"); @Test void testWithMySQL(@TempDir Path tempDir) throws SQLException { @@ -43,7 +44,7 @@ void testWithMySQL(@TempDir Path tempDir) throws SQLException { config.hostname = container.getHost(); config.port = container.getFirstMappedPort(); - databaseManager = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); + databaseManager = new DatabaseManager(this.logger, tempDir.toFile(), config); databaseManager.connect(); UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, this.testScheduler); @@ -71,31 +72,32 @@ void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { config.username = "sa"; config.password = ""; - // ✅ Use H2 in-memory database - config.hostname = null; // not used - config.port = 0; // not used - config.database = "eternalcode"; // any name works + config.hostname = null; + config.port = 0; + config.database = "eternalcode"; DatabaseManager db = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); db.connect(); UserRepository userRepo = new UserRepositoryOrmLite(db, new TestScheduler()); - // Insert 5000 users for (int i = 0; i < 50000; i++) { userRepo.saveUser(new User(UUID.randomUUID(), "user" + i)).join(); } + IntegrationTestSpec spec = new IntegrationTestSpec(); + long start = System.nanoTime(); - var allUsers = userRepo.fetchAllUsers().join(); + var allUsers = spec.await(userRepo.fetchAllUsers()); long allFetchTime = System.nanoTime() - start; start = System.nanoTime(); - var batchedUsers = userRepo.fetchUsersBatch(500).join(); + var batchedUsers = spec.await(userRepo.fetchUsersBatch(500)); long batchFetchTime = System.nanoTime() - start; - System.out.printf("All fetch: %d ms, Batched fetch: %d ms%n", - allFetchTime / 1_000_000, batchFetchTime / 1_000_000); + + this.logger.info(String.format("All users fetch time: %d ms", allFetchTime / 1_000_000)); + this.logger.info(String.format("Batched users fetch time: %d ms", batchFetchTime / 1_000_000)); Assertions.assertEquals(allUsers.size(), batchedUsers.size()); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java index 5f08834eb..7a8be3638 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java @@ -7,7 +7,7 @@ public class IntegrationTestSpec { public T await(CompletableFuture future) { return future - .orTimeout(5, TimeUnit.SECONDS) + .orTimeout(10, TimeUnit.SECONDS) .join(); } } From 9414f7d52de3ca851192dca9ac2027fcaf4c2fe9 Mon Sep 17 00:00:00 2001 From: Rollczi Date: Mon, 1 Sep 2025 22:57:56 +0200 Subject: [PATCH 5/8] wip --- .../FullServerBypassController.java | 24 ++-------- .../core/feature/msg/MsgServiceImpl.java | 32 ++++++------- .../litecommand/argument/UserArgument.java | 23 +++++++--- .../core/user/LoadUserController.java | 2 +- .../core/user/PrepareUserController.java | 31 +------------ .../java/com/eternalcode/core/user/User.java | 9 ---- .../core/user/UserClientBukkitSettings.java | 45 ------------------- .../core/user/UserClientNoneSettings.java | 11 ----- .../core/user/UserClientSettings.java | 13 ------ .../eternalcode/core/user/UserManager.java | 6 +-- .../core/user/database/UserRepository.java | 6 +-- .../core/user/PrepareUserControllerTest.java | 2 +- .../core/user/UserManagerTest.java | 3 +- 13 files changed, 48 insertions(+), 159 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java index dc9b388d7..523cf3188 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java @@ -5,8 +5,6 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Controller; import com.eternalcode.core.translation.TranslationManager; -import com.eternalcode.core.user.User; -import com.eternalcode.core.user.UserManager; import com.eternalcode.commons.adventure.AdventureUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -16,8 +14,6 @@ import org.bukkit.event.player.PlayerLoginEvent; import panda.utilities.text.Joiner; -import java.util.Optional; - @PermissionDocs( name = "Bypass Full Server", description = "This feature allows you to bypass the full server, example for vip rank.", @@ -29,13 +25,11 @@ class FullServerBypassController implements Listener { static final String SLOT_BYPASS = "eternalcore.slot.bypass"; private final TranslationManager translationManager; - private final UserManager userManager; private final MiniMessage miniMessage; @Inject - FullServerBypassController(TranslationManager translationManager, UserManager userManager, MiniMessage miniMessage) { + FullServerBypassController(TranslationManager translationManager, MiniMessage miniMessage) { this.translationManager = translationManager; - this.userManager = userManager; this.miniMessage = miniMessage; } @@ -50,26 +44,16 @@ void onLogin(PlayerLoginEvent event) { return; } - String serverFullMessage = this.getServerFullMessage(player); + String serverFullMessage = this.getServerFullMessage(); Component serverFullMessageComponent = this.miniMessage.deserialize(serverFullMessage); event.disallow(PlayerLoginEvent.Result.KICK_FULL, AdventureUtil.SECTION_SERIALIZER.serialize(serverFullMessageComponent)); } } - private String getServerFullMessage(Player player) { - Optional userOption = this.userManager.getUser(player.getUniqueId()); - - if (userOption.isEmpty()) { - return Joiner.on("\n") - .join(this.translationManager.getMessages().player().fullServerSlots()) - .toString(); - } - - User user = userOption.get(); - + private String getServerFullMessage() { return Joiner.on("\n") - .join(this.translationManager.getMessages(user.getUniqueId()).player().fullServerSlots()) + .join(this.translationManager.getMessages().player().fullServerSlots()) .toString(); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java index ae9b4566c..735f1aa3b 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import org.bukkit.Server; import org.bukkit.entity.Player; @Service @@ -27,6 +28,7 @@ class MsgServiceImpl implements MsgService { private final MsgPresenter presenter; private final EventCaller eventCaller; private final MsgToggleService msgToggleService; + private final Server server; private final Cache replies = CacheBuilder.newBuilder() .expireAfterWrite(Duration.ofHours(1)) @@ -40,7 +42,7 @@ class MsgServiceImpl implements MsgService { IgnoreService ignoreService, UserManager userManager, EventCaller eventCaller, - MsgToggleService msgToggleService + MsgToggleService msgToggleService, Server server ) { this.noticeService = noticeService; this.ignoreService = ignoreService; @@ -49,15 +51,10 @@ class MsgServiceImpl implements MsgService { this.msgToggleService = msgToggleService; this.presenter = new MsgPresenter(noticeService); + this.server = server; } void privateMessage(User sender, User target, String message) { - if (target.getClientSettings().isOffline()) { - this.noticeService.player(sender.getUniqueId(), translation -> translation.argument().offlinePlayer()); - - return; - } - UUID uniqueId = target.getUniqueId(); this.msgToggleService.getState(uniqueId).thenAccept(msgState -> { @@ -89,17 +86,14 @@ void reply(User sender, String message) { return; } - Optional targetOption = this.userManager.getUser(uuid); - - if (targetOption.isEmpty()) { + Player target = this.server.getPlayer(uuid); + if (target == null) { this.noticeService.player(sender.getUniqueId(), translation -> translation.argument().offlinePlayer()); return; } - User target = targetOption.get(); - - this.privateMessage(sender, target, message); + this.privateMessage(sender, toUser(target), message); } @Override @@ -119,12 +113,18 @@ public boolean isSpy(UUID player) { @Override public void reply(Player sender, String message) { - this.reply(this.userManager.getOrCreate(sender.getUniqueId(), sender.getName()), message); + this.reply(toUser(sender), message); } @Override public void sendMessage(Player sender, Player target, String message) { - User user = this.userManager.getOrCreate(target.getUniqueId(), target.getName()); - this.privateMessage(this.userManager.getOrCreate(sender.getUniqueId(), sender.getName()), user, message); + User user = toUser(target); + this.privateMessage(toUser(sender), user, message); + } + + private User toUser(Player target) { + return this.userManager.getOrCreate(target.getUniqueId(), target.getName()); } + + } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java index ff54a7a14..8a076a3b1 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java @@ -8,9 +8,12 @@ import com.eternalcode.core.user.UserManager; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; +import static dev.rollczi.litecommands.argument.parser.ParseResult.failure; +import static dev.rollczi.litecommands.argument.parser.ParseResult.success; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.suggestion.SuggestionContext; import dev.rollczi.litecommands.suggestion.SuggestionResult; +import java.util.regex.Pattern; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.HumanEntity; @@ -18,6 +21,8 @@ @LiteArgument(type = User.class) class UserArgument extends AbstractViewerArgument { + private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); + private final Server server; private final UserManager userManager; @@ -28,6 +33,17 @@ class UserArgument extends AbstractViewerArgument { this.userManager = userManager; } + @Override + public ParseResult parse(Invocation invocation, String argument, Translation translation) { + return ParseResult.completableFuture(this.userManager.getUser(argument), maybeUser -> maybeUser.map(user -> success(user)) + .orElse(failure(translation.argument().offlinePlayer()))); + } + + @Override + protected boolean match(Invocation invocation, Argument context, String argument) { + return USERNAME_PATTERN.matcher(argument).matches(); + } + @Override public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { return this.server.getOnlinePlayers().stream() @@ -35,11 +51,4 @@ public SuggestionResult suggest(Invocation invocation, Argument parse(Invocation invocation, String argument, Translation translation) { - return this.userManager.getUser(argument) - .map(ParseResult::success) - .orElseGet(() -> ParseResult.failure(translation.argument().offlinePlayer())); - } - } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java index db8e33b79..94072e517 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java @@ -27,7 +27,7 @@ void onReload(ServerLoadEvent event) { } for (Player player : this.server.getOnlinePlayers()) { - this.userManager.create(player.getUniqueId(), player.getName()); + this.userManager.getOrCreate(player.getUniqueId(), player.getName()); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java index 890667def..da575a4c6 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java @@ -2,52 +2,25 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Controller; -import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerQuitEvent; @Controller class PrepareUserController implements Listener { private final UserManager userManager; - private final Server server; @Inject - PrepareUserController(UserManager userManager, Server server) { + PrepareUserController(UserManager userManager) { this.userManager = userManager; - this.server = server; } @EventHandler void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - User user = this.userManager.getOrCreate(player.getUniqueId(), player.getName()); - UserClientBukkitSettings clientSettings = new UserClientBukkitSettings(this.server, user.getUniqueId()); - - user.setClientSettings(clientSettings); - } - - @EventHandler - void onQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - - User user = this.userManager.getUser(player.getUniqueId()) - .orElseThrow(() -> new IllegalStateException("User not found")); - - user.setClientSettings(UserClientSettings.NONE); + this.userManager.getOrCreate(player.getUniqueId(), player.getName()); } - @EventHandler - void onKick(PlayerKickEvent event) { - Player player = event.getPlayer(); - - User user = this.userManager.getUser(player.getUniqueId()) - .orElseThrow(() -> new IllegalStateException("User not found")); - - user.setClientSettings(UserClientSettings.NONE); - } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 12d6c45a4..1678a12be 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -7,7 +7,6 @@ public class User implements Viewer { - private UserClientSettings userClientSettings = UserClientSettings.NONE; private final String name; private final UUID uuid; @@ -32,14 +31,6 @@ public boolean isConsole() { return false; } - public UserClientSettings getClientSettings() { - return this.userClientSettings; - } - - public void setClientSettings(UserClientSettings userClientSettings) { - this.userClientSettings = userClientSettings; - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java deleted file mode 100644 index 088e4698e..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.eternalcode.core.user; - -import org.bukkit.Server; -import org.bukkit.entity.Player; -import panda.std.Option; - -import java.lang.ref.WeakReference; -import java.util.Locale; -import java.util.UUID; - -class UserClientBukkitSettings implements UserClientSettings { - - private final Server server; - private final UUID uuid; - private WeakReference playerReference; - - UserClientBukkitSettings(Server server, UUID uuid) { - this.server = server; - this.uuid = uuid; - this.playerReference = new WeakReference<>(server.getPlayer(uuid)); - } - - @Override - public boolean isOnline() { - return this.getPlayer().isPresent(); - } - - private Option getPlayer() { - Player player = this.playerReference.get(); - - if (player == null) { - Player playerFromServer = this.server.getPlayer(this.uuid); - - if (playerFromServer == null) { - return Option.none(); - } - - this.playerReference = new WeakReference<>(playerFromServer); - return Option.of(playerFromServer); - } - - return Option.of(player); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java deleted file mode 100644 index 62fdb60a0..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.eternalcode.core.user; - -class UserClientNoneSettings implements UserClientSettings { - - @Override - public boolean isOnline() { - return false; - } - - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java deleted file mode 100644 index 7a0012bf3..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eternalcode.core.user; - -public interface UserClientSettings { - - UserClientSettings NONE = new UserClientNoneSettings(); - - boolean isOnline(); - - default boolean isOffline() { - return !this.isOnline(); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index cfd37ed67..1c0fb4667 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -34,11 +34,11 @@ public UserManager(UserRepository userRepository, UserRepositorySettings userRep fetchUsers().thenRun(() -> this.fetched = true); } - public Optional getUser(UUID uuid) { + public CompletableFuture> getUser(UUID uuid) { return Optional.ofNullable(this.usersByUUID.getIfPresent(uuid)); } - public Optional getUser(String name) { + public CompletableFuture> getUser(String name) { return Optional.ofNullable(this.usersByName.getIfPresent(name.toLowerCase())); } @@ -62,7 +62,7 @@ public User getOrCreate(UUID uuid, String name) { return this.create(uuid, name); } - public User create(UUID uuid, String name) { + private User create(UUID uuid, String name) { if (this.usersByName.getIfPresent(name.toLowerCase()) != null || this.usersByUUID.getIfPresent(uuid) != null) { throw new IllegalStateException("User already exists"); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 3df08b53c..2e50b441d 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -13,9 +13,9 @@ public interface UserRepository { CompletableFuture saveUser(User player); - CompletableFuture updateUser(User player); - - CompletableFuture deleteUser(UUID uniqueId); +// CompletableFuture updateUser(User player); +// +// CompletableFuture deleteUser(UUID uniqueId); CompletableFuture> fetchAllUsers(); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java index 3aca45fa2..b9bd53fa7 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java @@ -32,7 +32,7 @@ void setUp() { this.mockServer = new MockServer(); this.userManager = new UserManager(mockUserRepository, mockUserRepositorySettings); - PrepareUserController controller = new PrepareUserController(this.userManager, this.mockServer.getServer()); + PrepareUserController controller = new PrepareUserController(this.userManager); this.mockServer.listenJoin(controller::onJoin); this.mockServer.listenQuit(controller::onQuit); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java index 57da100ca..52c165452 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java @@ -2,6 +2,7 @@ import com.eternalcode.core.test.MockUserRepository; import com.eternalcode.core.test.MockUserRepositorySettings; +import com.eternalcode.core.user.database.UserRepositoryConfig; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -12,7 +13,7 @@ class UserManagerTest { - private final MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); + private final UserRepositoryConfig mockUserRepositorySettings = new UserRepositoryConfig(); private final MockUserRepository mockUserRepository = new MockUserRepository(); @Test From b3efc5f3846d6bb7d5a20fe5959b028b5b54eea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 13 Sep 2025 08:58:01 +0200 Subject: [PATCH 6/8] wip2 --- .../java/com/eternalcode/core/user/User.java | 15 ++++++- .../eternalcode/core/user/UserManager.java | 5 ++- .../core/user/database/UserRepository.java | 3 +- .../user/database/UserRepositoryConfig.java | 3 ++ .../user/database/UserRepositoryOrmLite.java | 42 +++++-------------- .../user/database/UserRepositorySettings.java | 4 ++ .../core/user/database/UserTable.java | 16 +++++-- .../core/test/MockUserRepository.java | 3 +- .../core/test/MockUserRepositorySettings.java | 6 +++ .../core/user/UserBatchFetchTest.java | 3 +- 10 files changed, 59 insertions(+), 41 deletions(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 12d6c45a4..5cd4d39f1 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -2,6 +2,7 @@ import com.eternalcode.core.viewer.Viewer; +import java.time.Instant; import java.util.Objects; import java.util.UUID; @@ -11,10 +12,14 @@ public class User implements Viewer { private final String name; private final UUID uuid; + private final Instant created; + private final Instant lastLogin; - public User(UUID uuid, String name) { + public User(UUID uuid, String name, Instant created, Instant lastLogin) { this.name = name; this.uuid = uuid; + this.created = created; + this.lastLogin = lastLogin; } @Override @@ -27,6 +32,14 @@ public UUID getUniqueId() { return this.uuid; } + public Instant getCreated() { + return created; + } + + public Instant getLastLogin() { + return lastLogin; + } + @Override public boolean isConsole() { return false; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index cfd37ed67..a915e8541 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -6,6 +6,7 @@ import com.eternalcode.core.user.database.UserRepositorySettings; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Optional; @@ -67,7 +68,7 @@ public User create(UUID uuid, String name) { throw new IllegalStateException("User already exists"); } - User user = new User(uuid, name); + User user = new User(uuid, name, Instant.now(), Instant.now()); this.usersByUUID.put(uuid, user); this.usersByName.put(name.toLowerCase(), user); @@ -93,7 +94,7 @@ private CompletableFuture fetchUsers() { this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()).thenAccept(batchSave); } else { - this.userRepository.fetchAllUsers().thenAccept(batchSave); + this.userRepository.fetchAllUsers(this.userRepositorySettings.cacheLoadTreshold()).thenAccept(batchSave); } return CompletableFuture.completedFuture(null); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 3df08b53c..792ed8a2f 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -1,6 +1,7 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import java.time.Duration; import java.util.Collection; import java.util.Optional; import java.util.UUID; @@ -17,7 +18,7 @@ public interface UserRepository { CompletableFuture deleteUser(UUID uniqueId); - CompletableFuture> fetchAllUsers(); + CompletableFuture> fetchAllUsers(Duration fetchInPast); CompletableFuture> fetchUsersBatch(int batchSize); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java index 311c9b1e1..481ea1e89 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java @@ -2,6 +2,7 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; +import java.time.Duration; import lombok.Getter; import lombok.experimental.Accessors; @@ -21,4 +22,6 @@ public class UserRepositoryConfig extends OkaeriConfig implements UserRepository "# Value must be greater than 0!" }) public int batchDatabaseFetchSize = 1000; + + public Duration cacheLoadTreshold = Duration.ofDays(7); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java index 6d5d7f33c..808246606 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -8,6 +8,8 @@ import com.eternalcode.core.user.User; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -31,44 +33,20 @@ public CompletableFuture> getUser(UUID uniqueId) { } @Override - public CompletableFuture> fetchAllUsers() { - return this.selectAll(UserTable.class) - .thenApply(userTables -> userTables.stream() + public CompletableFuture> fetchAllUsers(Duration queryParameter) { + return this.selectAll(UserTable.class).thenApply(userTable -> + userTable.stream() .map(UserTable::toUser) - .toList()); - } - - @Override - public CompletableFuture> fetchUsersBatch(int batchSize) { - return CompletableFuture.supplyAsync(() -> { - try { - var users = new ArrayList(); - - int offset = 0; - while (true) { - List batch = this.selectBatch(UserTable.class, offset, batchSize).join(); - - if (batch.isEmpty()) { - break; - } - - batch.stream() - .map(UserTable::toUser) - .forEach(users::add); - - offset += batchSize; - } - - return users; - } catch (Exception exception) { - throw new RuntimeException("Failed to fetch users in batches", exception); - } - }); + .filter(user -> user.getLastLogin().isAfter(Instant.now().minus(queryParameter))) + .toList() + ); } @Override public CompletableFuture saveUser(User user) { return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> null); + + return this.upda } @Override diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java index e7ad7fa59..91247c426 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java @@ -1,8 +1,12 @@ package com.eternalcode.core.user.database; +import java.time.Duration; + public interface UserRepositorySettings { boolean useBatchDatabaseFetching(); int batchDatabaseFetchSize(); + + Duration cacheLoadTreshold(); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index a79f7d385..ecae808a9 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -3,6 +3,8 @@ import com.eternalcode.core.user.User; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import java.time.Duration; +import java.time.Instant; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -14,18 +16,26 @@ public class UserTable { @DatabaseField(columnName = "name") private String name; + @DatabaseField(columnName = "created") + private Instant created; + + @DatabaseField(columnName = "last_login") + private Instant lastLogin; + UserTable() {} - UserTable(UUID uniqueId, String name) { + UserTable(UUID uniqueId, String name, Instant created, Instant lastLogin) { this.uniqueId = uniqueId; this.name = name; + this.created = created; + this.lastLogin = lastLogin; } public User toUser() { - return new User(this.uniqueId, this.name); + return new User(this.uniqueId, this.name, this.created, this.lastLogin); } public static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName()); + return new UserTable(user.getUniqueId(), user.getName(), user.getCreated(), user.getLastLogin()); } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java index f5f3bb26d..b614082a8 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -2,6 +2,7 @@ import com.eternalcode.core.user.User; import com.eternalcode.core.user.database.UserRepository; +import java.time.Duration; import java.util.Collection; import java.util.Optional; import java.util.UUID; @@ -29,7 +30,7 @@ public CompletableFuture deleteUser(UUID uniqueId) { } @Override - public CompletableFuture> fetchAllUsers() { + public CompletableFuture> fetchAllUsers(Duration timeout) { return CompletableFuture.completedFuture(java.util.List.of()); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java index bf2c244ed..78671ebe6 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java @@ -1,6 +1,7 @@ package com.eternalcode.core.test; import com.eternalcode.core.user.database.UserRepositorySettings; +import java.time.Duration; public class MockUserRepositorySettings implements UserRepositorySettings { @@ -13,4 +14,9 @@ public boolean useBatchDatabaseFetching() { public int batchDatabaseFetchSize() { return 100; } + + @Override + public Duration cacheLoadTreshold() { + return Duration.ofDays(7); + } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java index 0f04f7fd6..759e112d1 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java @@ -10,6 +10,7 @@ import com.eternalcode.core.util.TestScheduler; import java.nio.file.Path; import java.sql.SQLException; +import java.time.Duration; import java.util.UUID; import java.util.logging.Logger; import org.junit.jupiter.api.Assertions; @@ -88,7 +89,7 @@ void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { IntegrationTestSpec spec = new IntegrationTestSpec(); long start = System.nanoTime(); - var allUsers = spec.await(userRepo.fetchAllUsers()); + var allUsers = spec.await(userRepo.fetchAllUsers(Duration.ofDays(7))); long allFetchTime = System.nanoTime() - start; start = System.nanoTime(); From 0cdf8b2d995b7893e0e826853dc4ef1eed253f23 Mon Sep 17 00:00:00 2001 From: CitralFlo Date: Sun, 5 Oct 2025 17:22:05 +0200 Subject: [PATCH 7/8] wip --- .../implementation/PluginConfiguration.java | 1 - .../core/user/PrepareUserController.java | 1 - .../eternalcode/core/user/UserManager.java | 78 +++++-------------- .../core/user/database/UserRepository.java | 13 ++-- .../user/database/UserRepositoryOrmLite.java | 53 ++++++++----- .../user/database/UserRepositorySettings.java | 12 --- .../core/user/database/UserTable.java | 18 ++--- .../core/test/MockUserRepository.java | 23 ++---- .../core/test/MockUserRepositorySettings.java | 1 - 9 files changed, 77 insertions(+), 123 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index 47bfcce51..b7105f2da 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -42,7 +42,6 @@ import com.eternalcode.core.translation.TranslationConfig; import com.eternalcode.core.translation.TranslationSettings; import com.eternalcode.core.user.database.UserRepositoryConfig; -import com.eternalcode.core.user.database.UserRepositorySettings; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java index da575a4c6..5f02253e5 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java @@ -20,7 +20,6 @@ class PrepareUserController implements Listener { @EventHandler void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - this.userManager.getOrCreate(player.getUniqueId(), player.getName()); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 461a6bd85..5d9861651 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -3,7 +3,6 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; -import com.eternalcode.core.user.database.UserRepositorySettings; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.time.Instant; @@ -19,84 +18,49 @@ public class UserManager { private final Cache usersByUUID; private final Cache usersByName; - private boolean fetched = false; private final UserRepository userRepository; - private final UserRepositorySettings userRepositorySettings; @Inject - public UserManager(UserRepository userRepository, UserRepositorySettings userRepositorySettings) { - this.userRepositorySettings = userRepositorySettings; + public UserManager(UserRepository userRepository) { this.usersByUUID = Caffeine.newBuilder().build(); this.usersByName = Caffeine.newBuilder().build(); this.userRepository = userRepository; - - fetchUsers().thenRun(() -> this.fetched = true); + this.fetchActiveUsers(); } - public CompletableFuture> getUser(UUID uuid) { - return Optional.ofNullable(this.usersByUUID.getIfPresent(uuid)); + public Optional getUser(UUID uniqueId) { + return Optional.ofNullable(this.usersByUUID.getIfPresent(uniqueId)); } - public CompletableFuture> getUser(String name) { - return Optional.ofNullable(this.usersByName.getIfPresent(name.toLowerCase())); + public Optional getUser(String name) { + return Optional.ofNullable(this.usersByName.getIfPresent(name)); } - public User getOrCreate(UUID uuid, String name) { - if (!this.fetched) { - throw new IllegalStateException("Users have not been fetched from the database yet!"); - } - - User userByUUID = this.usersByUUID.getIfPresent(uuid); - - if (userByUUID != null) { - return userByUUID; - } - - User userByName = this.usersByName.getIfPresent(name.toLowerCase()); - - if (userByName != null) { - return userByName; - } - - return this.create(uuid, name); + public CompletableFuture> getUserFromRepository(UUID uniqueId) { + return this.userRepository.getUser(uniqueId); } - private User create(UUID uuid, String name) { - if (this.usersByName.getIfPresent(name.toLowerCase()) != null || this.usersByUUID.getIfPresent(uuid) != null) { - throw new IllegalStateException("User already exists"); - } - - User user = new User(uuid, name, Instant.now(), Instant.now()); - this.usersByUUID.put(uuid, user); - this.usersByName.put(name.toLowerCase(), user); + public CompletableFuture> getUserFromRepository(String name) { + return this.userRepository.getUser(name); + } + public void saveUser(User user) { + this.saveInCache(user); this.userRepository.saveUser(user); - return user; } - public Collection getUsers() { - return Collections.unmodifiableCollection(this.usersByUUID.asMap().values()); + public void updateLastSeen(UUID uniqueId, String name) { + this.userRepository.updateUser(uniqueId, name).thenAccept(this::saveInCache); } - private CompletableFuture fetchUsers() { - if (this.userRepositorySettings.batchDatabaseFetchSize() <= 0) { - throw new IllegalArgumentException("Value for batchDatabaseFetchSize must be greater than 0!"); - } - - Consumer> batchSave = users -> users.forEach(user -> { - this.usersByName.put(user.getName(), user); - this.usersByUUID.put(user.getUniqueId(), user); - }); - - if (this.userRepositorySettings.useBatchDatabaseFetching()) { - this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()).thenAccept(batchSave); - } - else { - this.userRepository.fetchAllUsers(this.userRepositorySettings.cacheLoadTreshold()).thenAccept(batchSave); - } + private void fetchActiveUsers() { + this.userRepository.getActiveUsers().thenAccept(list -> list.forEach(this::saveInCache)); + } - return CompletableFuture.completedFuture(null); + private void saveInCache(User user) { + this.usersByUUID.put(user.getUniqueId(), user); + this.usersByName.put(user.getName(), user); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index f0a5e0075..75393fc10 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -3,6 +3,7 @@ import com.eternalcode.core.user.User; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -10,15 +11,13 @@ public interface UserRepository { - CompletableFuture> getUser(UUID uniqueId); + CompletableFuture> getActiveUsers(); - CompletableFuture saveUser(User player); + CompletableFuture> getUser(UUID uniqueId); -// CompletableFuture updateUser(User player); -// -// CompletableFuture deleteUser(UUID uniqueId); + CompletableFuture> getUser(String name); - CompletableFuture> fetchAllUsers(Duration fetchInPast); + CompletableFuture saveUser(User user); - CompletableFuture> fetchUsersBatch(int batchSize); + CompletableFuture updateUser(UUID uniqueId, String name); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java index 808246606..50ff908e6 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -7,11 +7,11 @@ import com.eternalcode.core.injector.annotations.component.Repository; import com.eternalcode.core.user.User; import com.j256.ormlite.table.TableUtils; +import org.jetbrains.annotations.Blocking; + import java.sql.SQLException; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -20,43 +20,56 @@ @Repository public class UserRepositoryOrmLite extends AbstractRepositoryOrmLite implements UserRepository { + private static final Duration WEEK = Duration.ofDays(7); + private static final String NAME_COLUMN = "name"; + @Inject public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { super(databaseManager, scheduler); TableUtils.createTableIfNotExists(databaseManager.connectionSource(), UserTable.class); } + @Blocking + public CompletableFuture> getActiveUsers() { + return this.selectAll(UserTable.class) + .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()) + .thenApply(users -> users.stream().filter(user -> user.getLastLogin().isAfter(Instant.now().minus(WEEK))).toList()); + } + @Override + @Blocking public CompletableFuture> getUser(UUID uniqueId) { return this.selectSafe(UserTable.class, uniqueId) .thenApply(optional -> optional.map(UserTable::toUser)); } @Override - public CompletableFuture> fetchAllUsers(Duration queryParameter) { - return this.selectAll(UserTable.class).thenApply(userTable -> - userTable.stream() - .map(UserTable::toUser) - .filter(user -> user.getLastLogin().isAfter(Instant.now().minus(queryParameter))) - .toList() + @Blocking + public CompletableFuture> getUser(String name) { + return this.action(UserTable.class, dao -> Optional.ofNullable(dao.queryBuilder() + .where() + .eq(NAME_COLUMN, name) + .queryForFirst().toUser()) ); } @Override - public CompletableFuture saveUser(User user) { - return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> null); - - return this.upda - } - - @Override - public CompletableFuture updateUser(User user) { - return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> user); + @Blocking + public CompletableFuture saveUser(User user) { + return this.saveIfNotExist(UserTable.class, UserTable.from(user)).thenApply(UserTable::toUser); } @Override - public CompletableFuture deleteUser(UUID uniqueId) { - return this.deleteById(UserTable.class, uniqueId).thenApply(v -> null); + @Blocking + public CompletableFuture updateUser(UUID uniqueId, String name) { + Instant now = Instant.now(); + return this.selectSafe(UserTable.class, uniqueId) + .thenApply(optional -> optional.map(UserTable::toUser)) + .thenApply(optionalUser -> optionalUser.orElse(new User(uniqueId, name, now, now))) + .thenApply(user -> new User(user.getUniqueId(), user.getName(), user.getCreated(), now)) + .thenApply(user -> { + this.save(UserTable.class, UserTable.from(user)); + return user; + }); } - } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java deleted file mode 100644 index 91247c426..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.eternalcode.core.user.database; - -import java.time.Duration; - -public interface UserRepositorySettings { - - boolean useBatchDatabaseFetching(); - - int batchDatabaseFetchSize(); - - Duration cacheLoadTreshold(); -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index ecae808a9..2816f62bc 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -1,10 +1,10 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; -import java.time.Duration; -import java.time.Instant; +import java.util.Date; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -16,15 +16,15 @@ public class UserTable { @DatabaseField(columnName = "name") private String name; - @DatabaseField(columnName = "created") - private Instant created; + @DatabaseField(columnName = "created", dataType = DataType.DATE) + private Date created; - @DatabaseField(columnName = "last_login") - private Instant lastLogin; + @DatabaseField(columnName = "last_login", dataType = DataType.DATE) + private Date lastLogin; UserTable() {} - UserTable(UUID uniqueId, String name, Instant created, Instant lastLogin) { + UserTable(UUID uniqueId, String name, Date created, Date lastLogin) { this.uniqueId = uniqueId; this.name = name; this.created = created; @@ -32,10 +32,10 @@ public class UserTable { } public User toUser() { - return new User(this.uniqueId, this.name, this.created, this.lastLogin); + return new User(this.uniqueId, this.name, this.created.toInstant(), this.lastLogin.toInstant()); } public static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName(), user.getCreated(), user.getLastLogin()); + return new UserTable(user.getUniqueId(), user.getName(), Date.from(user.getCreated()), Date.from(user.getLastLogin())); } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java index b614082a8..1efec7eda 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -4,38 +4,31 @@ import com.eternalcode.core.user.database.UserRepository; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class MockUserRepository implements UserRepository { - @Override - public CompletableFuture> getUser(UUID uniqueId) { - return CompletableFuture.completedFuture(null); - } @Override - public CompletableFuture saveUser(User player) { - return CompletableFuture.completedFuture(null); + public CompletableFuture> getActiveUsers() { + return null; } @Override - public CompletableFuture updateUser(User player) { - return CompletableFuture.completedFuture(player); + public CompletableFuture> getUser(UUID uniqueId) { + return CompletableFuture.completedFuture(null); } @Override - public CompletableFuture deleteUser(UUID uniqueId) { + public CompletableFuture saveUser(User player) { return CompletableFuture.completedFuture(null); } @Override - public CompletableFuture> fetchAllUsers(Duration timeout) { - return CompletableFuture.completedFuture(java.util.List.of()); + public CompletableFuture updateUser(UUID uniqueId, String name) { + return CompletableFuture.completedFuture(null); } - @Override - public CompletableFuture> fetchUsersBatch(int batchSize) { - return CompletableFuture.completedFuture(java.util.List.of()); - } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java index 78671ebe6..68b469856 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java @@ -1,6 +1,5 @@ package com.eternalcode.core.test; -import com.eternalcode.core.user.database.UserRepositorySettings; import java.time.Duration; public class MockUserRepositorySettings implements UserRepositorySettings { From 11813d4534469f7f1407548815bef14b48c16ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Mon, 6 Oct 2025 19:20:49 +0200 Subject: [PATCH 8/8] Update methods and failsafe for cache --- .../implementation/PluginConfiguration.java | 7 ---- .../core/feature/afk/AfkKickController.java | 4 +- .../core/feature/msg/MsgServiceImpl.java | 3 +- .../litecommand/argument/UserArgument.java | 2 +- .../core/user/LoadUserController.java | 34 --------------- .../core/user/PrepareUserController.java | 25 ----------- .../eternalcode/core/user/UserController.java | 42 +++++++++++++++++++ .../eternalcode/core/user/UserManager.java | 40 +++++++++++++++--- .../user/database/UserRepositoryConfig.java | 27 ------------ .../core/test/MockUserRepositorySettings.java | 21 ---------- 10 files changed, 81 insertions(+), 124 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index b7105f2da..5370be646 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -41,7 +41,6 @@ import com.eternalcode.core.injector.annotations.component.ConfigurationFile; import com.eternalcode.core.translation.TranslationConfig; import com.eternalcode.core.translation.TranslationSettings; -import com.eternalcode.core.user.database.UserRepositoryConfig; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; @@ -80,12 +79,6 @@ public class PluginConfiguration extends AbstractConfigurationFile { @Comment("# Settings responsible for the database connection") DatabaseConfig database = new DatabaseConfig(); - @Bean(proxied = UserRepositorySettings.class) - @Comment("") - @Comment("# User Repository Configuration") - @Comment("# Settings for managing user data storage and retrieval") - UserRepositoryConfig userRepository = new UserRepositoryConfig(); - @Bean(proxied = SpawnJoinSettings.class) @Comment("") @Comment("# Spawn & Join Configuration") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java index c1bc2c32a..fd9a1441b 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java @@ -9,6 +9,7 @@ import com.eternalcode.core.translation.TranslationManager; import com.eternalcode.core.user.User; import com.eternalcode.core.user.UserManager; +import java.util.Optional; import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -56,7 +57,8 @@ void onAfkSwitch(AfkSwitchEvent event) { return; } - User user = this.userManager.getOrCreate(playerUUID, player.getName()); + Optional optionalUser = this.userManager.getUser(playerUUID); + User user = optionalUser.get(); Translation translation = this.translationManager.getMessages(user.getUniqueId()); Component component = this.miniMessage.deserialize(translation.afk().afkKickReason()); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java index 735f1aa3b..18ae9bea2 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java @@ -13,7 +13,6 @@ import com.google.common.cache.CacheBuilder; import java.time.Duration; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.UUID; import org.bukkit.Server; @@ -123,7 +122,7 @@ public void sendMessage(Player sender, Player target, String message) { } private User toUser(Player target) { - return this.userManager.getOrCreate(target.getUniqueId(), target.getName()); + return this.userManager.getUser(target.getUniqueId()).get(); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java index 8a076a3b1..883885d68 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java @@ -35,7 +35,7 @@ class UserArgument extends AbstractViewerArgument { @Override public ParseResult parse(Invocation invocation, String argument, Translation translation) { - return ParseResult.completableFuture(this.userManager.getUser(argument), maybeUser -> maybeUser.map(user -> success(user)) + return ParseResult.completableFuture(this.userManager.getUserFromRepository(argument), maybeUser -> maybeUser.map(user -> success(user)) .orElse(failure(translation.argument().offlinePlayer()))); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java deleted file mode 100644 index 94072e517..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.injector.annotations.Inject; -import com.eternalcode.core.injector.annotations.component.Controller; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.server.ServerLoadEvent; - -@Controller -class LoadUserController implements Listener { - - private final UserManager userManager; - private final Server server; - - @Inject - LoadUserController(UserManager userManager, Server server) { - this.userManager = userManager; - this.server = server; - } - - @EventHandler - void onReload(ServerLoadEvent event) { - if (event.getType() != ServerLoadEvent.LoadType.RELOAD) { - return; - } - - for (Player player : this.server.getOnlinePlayers()) { - this.userManager.getOrCreate(player.getUniqueId(), player.getName()); - } - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java deleted file mode 100644 index 5f02253e5..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.injector.annotations.Inject; -import com.eternalcode.core.injector.annotations.component.Controller; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; - -@Controller -class PrepareUserController implements Listener { - - private final UserManager userManager; - - @Inject - PrepareUserController(UserManager userManager) { - this.userManager = userManager; - } - - @EventHandler - void onJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java new file mode 100644 index 000000000..ab082d279 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java @@ -0,0 +1,42 @@ +package com.eternalcode.core.user; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Controller; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.server.ServerLoadEvent; + +@Controller +public class UserController { + + private final UserManager userManager; + private final Server server; + + @Inject + public UserController(UserManager userManager, Server server) { + this.userManager = userManager; + this.server = server; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + this.userManager.fetchUser(event.getPlayer().getUniqueId()); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + this.userManager.updateLastSeen(player.getUniqueId(), player.getName()); + } + + @EventHandler + public void onReload(ServerLoadEvent event) { + if (event.getType() == ServerLoadEvent.LoadType.RELOAD) { + this.server.getOnlinePlayers().forEach(player -> this.userManager.updateLastSeen(player.getUniqueId(), player.getName())); + } + } + +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 5d9861651..bb91eb0ef 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -5,13 +5,9 @@ import com.eternalcode.core.user.database.UserRepository; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; @Service public class UserManager { @@ -39,11 +35,37 @@ public Optional getUser(String name) { } public CompletableFuture> getUserFromRepository(UUID uniqueId) { - return this.userRepository.getUser(uniqueId); + + User userFromCache = this.usersByUUID.getIfPresent(uniqueId); + + if (userFromCache != null) { + return CompletableFuture.completedFuture(Optional.of(userFromCache)); + } + + CompletableFuture> userFuture = this.userRepository.getUser(uniqueId); + userFuture.thenAccept(optionalUser -> optionalUser.ifPresent(user -> { + this.usersByUUID.put(uniqueId, user); + this.usersByName.put(user.getName(), user); + })); + + return userFuture; } public CompletableFuture> getUserFromRepository(String name) { - return this.userRepository.getUser(name); + + User userFromCache = this.usersByName.getIfPresent(name); + + if (userFromCache != null) { + return CompletableFuture.completedFuture(Optional.of(userFromCache)); + } + + CompletableFuture> userFuture = this.userRepository.getUser(name); + userFuture.thenAccept(optionalUser -> optionalUser.ifPresent(user -> { + this.usersByUUID.put(user.getUniqueId(), user); + this.usersByName.put(name, user); + })); + + return userFuture; } public void saveUser(User user) { @@ -59,6 +81,12 @@ private void fetchActiveUsers() { this.userRepository.getActiveUsers().thenAccept(list -> list.forEach(this::saveInCache)); } + void fetchUser(UUID uniqueId) { + this.userRepository.getUser(uniqueId).thenAccept(optionalUser -> { + optionalUser.ifPresent(user -> this.saveInCache(user)); + }); + } + private void saveInCache(User user) { this.usersByUUID.put(user.getUniqueId(), user); this.usersByName.put(user.getName(), user); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java deleted file mode 100644 index 481ea1e89..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.eternalcode.core.user.database; - -import eu.okaeri.configs.OkaeriConfig; -import eu.okaeri.configs.annotation.Comment; -import java.time.Duration; -import lombok.Getter; -import lombok.experimental.Accessors; - -@Getter -@Accessors(fluent = true) -public class UserRepositoryConfig extends OkaeriConfig implements UserRepositorySettings { - - @Comment({ - "# Should plugin use batches to fetch users from the database?", - "# We suggest turning this setting to TRUE for servers with more than 10k users", - "# Set this to false if you are using SQLITE or H2 database (local databases)" - }) - public boolean useBatchDatabaseFetching = false; - - @Comment({ - "# Size of batches querried to the database", - "# Value must be greater than 0!" - }) - public int batchDatabaseFetchSize = 1000; - - public Duration cacheLoadTreshold = Duration.ofDays(7); -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java deleted file mode 100644 index 68b469856..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.eternalcode.core.test; - -import java.time.Duration; - -public class MockUserRepositorySettings implements UserRepositorySettings { - - @Override - public boolean useBatchDatabaseFetching() { - return false; - } - - @Override - public int batchDatabaseFetchSize() { - return 100; - } - - @Override - public Duration cacheLoadTreshold() { - return Duration.ofDays(7); - } -}