mirror of
https://github.com/DiscordSRV/Ascension.git
synced 2024-11-01 08:39:31 +01:00
Storage
This commit is contained in:
parent
ba1a8c196f
commit
bc60b842c3
@ -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 {
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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<>();
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.discordsrv.common.function;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface CheckedConsumer<I> {
|
||||||
|
|
||||||
|
void accept(I input) throws Throwable;
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
package com.discordsrv.common.storage.impl;
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user