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 StorageConfig storage = new StorageConfig();
|
||||
|
||||
@ConfigSerializable
|
||||
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 dev.vankka.dependencydownload.DependencyManager;
|
||||
import dev.vankka.dependencydownload.classloader.IsolatedClassLoader;
|
||||
import dev.vankka.dependencydownload.classpath.ClasspathAppender;
|
||||
import dev.vankka.dependencydownload.repository.StandardRepository;
|
||||
|
||||
@ -70,6 +71,12 @@ public class DependencyLoader {
|
||||
return download(dependencyManager, classpathAppender);
|
||||
}
|
||||
|
||||
public IsolatedClassLoader loadIntoIsolated() throws IOException {
|
||||
IsolatedClassLoader classLoader = new IsolatedClassLoader();
|
||||
process(classLoader).join();
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> download(DependencyManager manager,
|
||||
ClasspathAppender classpathAppender) {
|
||||
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
|
||||
public interface Storage {
|
||||
|
||||
void initialize();
|
||||
void close();
|
||||
|
||||
@Nullable
|
||||
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