This commit is contained in:
Vankka 2022-02-19 22:44:29 +02:00
parent ba1a8c196f
commit bc60b842c3
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
11 changed files with 452 additions and 0 deletions

View File

@ -34,6 +34,8 @@ public class ConnectionConfig implements Config {
public Bot bot = new Bot(); public Bot bot = new Bot();
public StorageConfig storage = new StorageConfig();
@ConfigSerializable @ConfigSerializable
public static class Bot { public static class Bot {

View File

@ -0,0 +1,80 @@
package com.discordsrv.common.config.connection;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
@ConfigSerializable
public class StorageConfig {
@Comment("The storage backend to use.\n\n"
+ "- H2\n"
+ "- MySQL\n")
public String backend = "h2";
@Comment("Connection options for remote databases (MySQL)")
public Remote remote = new Remote();
@Comment("Extra connection properties for database drivers")
public Map<String, String> driverProperties = new LinkedHashMap<String, String>() {{
put("useSSL", "false");
}};
public Properties getDriverProperties() {
Properties properties = new Properties();
for (Map.Entry<String, String> property : driverProperties.entrySet()) {
String key = property.getKey();
String value = property.getValue();
if (value.equals("true")) {
properties.put(key, true);
} else if (value.equals("false")) {
properties.put(key, false);
} else {
properties.put(key, value);
}
}
return properties;
}
public static class Remote {
@Comment("The database address.\n"
+ "Uses the default port (MySQL: 3306)\n"
+ "for the database if a port isn't specified in the \"address:port\" format")
public String databaseAddress = "localhost";
@Comment("The name of the database")
public String databaseName = "minecraft";
@Comment("The database username and password")
public String username = "root";
public String password = "";
@Comment("Connection pool options. Don't touch these unless you know what you're doing")
public Pool poolOptions = new Pool();
}
public static class Pool {
@Comment("The maximum amount of concurrent connections to keep to the database")
public int maximumPoolSize = 5;
@Comment("The minimum amount of concurrent connections to keep to the database")
public int minimumPoolSize = 2;
@Comment("How frequently to attempt to keep connections alive, in order to prevent being timed out by the database or network infrastructure.\n"
+ "The time is specified in milliseconds. Use 0 to disable keepalive."
+ "The default is 0 (disabled)")
public long keepaliveTime = 0;
@Comment("The maximum time a connection will be kept open in milliseconds.\n"
+ "The time is specified in milliseconds. Must be at least 30000ms (30 seconds)"
+ "The default is 1800000ms (30 minutes)")
public long maximumLifetime = 1800000;
}
}

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.dependency;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import dev.vankka.dependencydownload.DependencyManager; import dev.vankka.dependencydownload.DependencyManager;
import dev.vankka.dependencydownload.classloader.IsolatedClassLoader;
import dev.vankka.dependencydownload.classpath.ClasspathAppender; import dev.vankka.dependencydownload.classpath.ClasspathAppender;
import dev.vankka.dependencydownload.repository.StandardRepository; import dev.vankka.dependencydownload.repository.StandardRepository;
@ -70,6 +71,12 @@ public class DependencyLoader {
return download(dependencyManager, classpathAppender); return download(dependencyManager, classpathAppender);
} }
public IsolatedClassLoader loadIntoIsolated() throws IOException {
IsolatedClassLoader classLoader = new IsolatedClassLoader();
process(classLoader).join();
return classLoader;
}
private CompletableFuture<Void> download(DependencyManager manager, private CompletableFuture<Void> download(DependencyManager manager,
ClasspathAppender classpathAppender) { ClasspathAppender classpathAppender) {
CompletableFuture<Void> future = new CompletableFuture<>(); CompletableFuture<Void> future = new CompletableFuture<>();

View File

@ -0,0 +1,12 @@
package com.discordsrv.common.exception;
public class StorageException extends RuntimeException {
public StorageException(Throwable cause) {
super(cause);
}
public StorageException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.discordsrv.common.function;
@FunctionalInterface
public interface CheckedConsumer<I> {
void accept(I input) throws Throwable;
}

View File

@ -27,6 +27,9 @@ import java.util.UUID;
@Blocking @Blocking
public interface Storage { public interface Storage {
void initialize();
void close();
@Nullable @Nullable
Long getUserId(@NotNull UUID player); Long getUserId(@NotNull UUID player);

View File

@ -0,0 +1 @@
package com.discordsrv.common.storage.impl;

View File

@ -0,0 +1,112 @@
package com.discordsrv.common.storage.impl.sql;
import com.discordsrv.common.exception.StorageException;
import com.discordsrv.common.function.CheckedConsumer;
import com.discordsrv.common.function.CheckedFunction;
import com.discordsrv.common.storage.Storage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.UUID;
public abstract class SQLStorage implements Storage {
public abstract Connection getConnection();
public abstract boolean isAutoCloseConnections();
private void useConnection(CheckedConsumer<Connection> connectionConsumer) throws StorageException {
useConnection(connection -> {
connectionConsumer.accept(connection);
return null;
});
}
private <T> T useConnection(CheckedFunction<Connection, T> connectionFunction) throws StorageException {
try {
if (isAutoCloseConnections()) {
try (Connection connection = getConnection()) {
return connectionFunction.apply(connection);
}
} else {
return connectionFunction.apply(getConnection());
}
} catch (Throwable e) {
throw new StorageException(e);
}
}
private void exceptEffectedRows(int rows, int expect) {
if (rows != expect) {
throw new StorageException("Excepted to effect " + expect + " rows, actually effected " + rows);
}
}
@Override
public void initialize() {
useConnection(connection -> {
try (Statement statement = connection.createStatement()) {
statement.execute("create table if not exists LINKED_ACCOUNTS (ID int not null auto_increment, PLAYER_UUID uuid, USER_ID bigint)");
}
});
}
@Override
public @Nullable Long getUserId(@NotNull UUID player) {
return useConnection(connection -> {
try (PreparedStatement statement = connection.prepareStatement("select USER_ID from LINKED_ACCOUNTS where PLAYER_UUID = ?;")) {
statement.setObject(1, player);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getLong("USER_ID");
}
}
}
return null;
});
}
@Override
public @Nullable UUID getPlayerUUID(long userId) {
return useConnection(connection -> {
try (PreparedStatement statement = connection.prepareStatement("select PLAYER_UUID from LINKED_ACCOUNTS where USER_ID = ?;")) {
statement.setLong(1, userId);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getObject("PLAYER_UUID", UUID.class);
}
}
}
return null;
});
}
@Override
public void link(@NotNull UUID player, long userId) {
useConnection(connection -> {
try (PreparedStatement statement = connection.prepareStatement("insert into LINKED_ACCOUNTS (PLAYER_UUID, USER_ID) values (?, ?);")) {
statement.setObject(1, player);
statement.setLong(2, userId);
exceptEffectedRows(statement.executeUpdate(), 1);
}
});
}
@Override
public int getLinkedAccountCount() {
return useConnection(connection -> {
try (Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery("select count(*) from LINKED_ACCOUNTS;")) {
if (resultSet.next()) {
return resultSet.getInt(1);
}
}
}
return 0;
});
}
}

View File

@ -0,0 +1,83 @@
package com.discordsrv.common.storage.impl.sql.file;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.StorageConfig;
import com.discordsrv.common.dependency.DependencyLoader;
import com.discordsrv.common.exception.StorageException;
import com.discordsrv.common.storage.impl.sql.SQLStorage;
import dev.vankka.dependencydownload.classloader.IsolatedClassLoader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class H2Storage extends SQLStorage {
private final DiscordSRV discordSRV;
private IsolatedClassLoader classLoader;
private Connection connection;
public H2Storage(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
}
@Override
public void initialize() {
try {
classLoader = DependencyLoader.h2(discordSRV).loadIntoIsolated();
} catch (IOException e) {
throw new StorageException(e);
}
StorageConfig storageConfig = discordSRV.connectionConfig().storage;
try {
Class<?> clazz = classLoader.loadClass("org.h2.jdbc.JdbcConnection");
Constructor<?> constructor = clazz.getConstructor(
String.class, // url
Properties.class, // info
String.class, // username
Object.class, // password
Boolean.class // forbidCreation
);
connection = (Connection) constructor.newInstance(
"jdbc:h2:" + discordSRV.dataDirectory().resolve("h2-database").toAbsolutePath(),
storageConfig.getDriverProperties(),
null,
null,
false
);
} catch (ReflectiveOperationException e) {
throw new StorageException(e);
}
super.initialize();
}
@Override
public void close() {
if (connection != null) {
try {
connection.close();
} catch (SQLException ignored) {}
}
if (classLoader != null) {
try {
classLoader.close();
} catch (IOException e) {
discordSRV.logger().error("Failed to close isolated classloader", e);
}
}
}
@Override
public synchronized Connection getConnection() {
return connection;
}
@Override
public boolean isAutoCloseConnections() {
return false;
}
}

View File

@ -0,0 +1,80 @@
package com.discordsrv.common.storage.impl.sql.hikari;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.StorageConfig;
import com.discordsrv.common.exception.StorageException;
import com.discordsrv.common.storage.impl.sql.SQLStorage;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public abstract class HikariStorage extends SQLStorage {
protected final DiscordSRV discordSRV;
private HikariDataSource hikariDataSource;
public HikariStorage(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
}
protected abstract void applyConfiguration(HikariConfig config, StorageConfig storageConfig);
protected <T extends ClassLoader> T initializeWithContext(T classLoader) {
Thread currentThread = Thread.currentThread();
ClassLoader originalContext = currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(classLoader);
initializeInternal();
} finally {
currentThread.setContextClassLoader(originalContext);
}
return classLoader;
}
private void initializeInternal() {
StorageConfig storageConfig = discordSRV.connectionConfig().storage;
StorageConfig.Remote remoteConfig = storageConfig.remote;
StorageConfig.Pool poolConfig = remoteConfig.poolOptions;
HikariConfig config = new HikariConfig();
config.setPoolName("discordsrv-pool");
config.setUsername(remoteConfig.username);
config.setPassword(remoteConfig.password);
config.setMinimumIdle(poolConfig.minimumPoolSize);
config.setMaximumPoolSize(poolConfig.maximumPoolSize);
config.setMaxLifetime(poolConfig.maximumLifetime);
config.setKeepaliveTime(poolConfig.keepaliveTime);
applyConfiguration(config, storageConfig);
hikariDataSource = new HikariDataSource(config);
super.initialize();
}
@Override
public void initialize() {
initializeInternal();
}
@Override
public void close() {
if (hikariDataSource != null) {
hikariDataSource.close();
}
}
@Override
public Connection getConnection() {
try {
return hikariDataSource.getConnection();
} catch (SQLException e) {
throw new StorageException(e);
}
}
@Override
public boolean isAutoCloseConnections() {
return true;
}
}

View File

@ -0,0 +1,65 @@
package com.discordsrv.common.storage.impl.sql.hikari;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.StorageConfig;
import com.discordsrv.common.dependency.DependencyLoader;
import com.discordsrv.common.exception.StorageException;
import com.zaxxer.hikari.HikariConfig;
import dev.vankka.dependencydownload.classloader.IsolatedClassLoader;
import java.io.IOException;
import java.util.Map;
public class MySQLStorage extends HikariStorage {
private IsolatedClassLoader classLoader;
public MySQLStorage(DiscordSRV discordSRV) {
super(discordSRV);
}
@Override
public void close() {
super.close();
if (classLoader != null) {
try {
classLoader.close();
} catch (IOException e) {
discordSRV.logger().error("Failed to close isolated classloader", e);
}
}
}
@Override
public void initialize() {
try {
classLoader = initializeWithContext(DependencyLoader.mysql(discordSRV).loadIntoIsolated());
} catch (IOException e) {
throw new StorageException(e);
}
}
@Override
protected void applyConfiguration(HikariConfig config, StorageConfig storageConfig) {
String address = storageConfig.remote.databaseAddress;
if (!address.contains(":")) {
address += ":3306";
}
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setJdbcUrl("jdbc:mysql://" + address + "/" + storageConfig.remote.databaseName);
for (Map.Entry<Object, Object> entry : storageConfig.getDriverProperties().entrySet()) {
config.addDataSourceProperty((String) entry.getKey(), entry.getValue());
}
// https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
config.addDataSourceProperty("prepStmtCacheSize", 250);
config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
config.addDataSourceProperty("cachePrepStmts", true);
config.addDataSourceProperty("useServerPrepStmts", true);
config.addDataSourceProperty("cacheServerConfiguration", true);
config.addDataSourceProperty("useLocalSessionState", true);
config.addDataSourceProperty("rewriteBatchedStatements", true);
config.addDataSourceProperty("maintainTimeStats", false);
}
}