Cleanup flatfile database handling

This commit is contained in:
Luck 2020-11-16 01:17:24 +00:00
parent c39749e526
commit d1b53f65ae
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
5 changed files with 173 additions and 168 deletions

View File

@ -42,7 +42,7 @@ import me.lucko.luckperms.common.storage.implementation.split.SplitStorage;
import me.lucko.luckperms.common.storage.implementation.split.SplitStorageType; import me.lucko.luckperms.common.storage.implementation.split.SplitStorageType;
import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage;
import me.lucko.luckperms.common.storage.implementation.sql.connection.file.H2ConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.file.H2ConnectionFactory;
import me.lucko.luckperms.common.storage.implementation.sql.connection.file.SQLiteConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.file.SqliteConnectionFactory;
import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MariaDbConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MariaDbConnectionFactory;
import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MySqlConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MySqlConnectionFactory;
import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.PostgreConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.PostgreConnectionFactory;
@ -108,13 +108,13 @@ public class StorageFactory {
case SQLITE: case SQLITE:
return new SqlStorage( return new SqlStorage(
this.plugin, this.plugin,
new SQLiteConnectionFactory(this.plugin, this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-sqlite.db")), new SqliteConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-sqlite.db")),
this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX)
); );
case H2: case H2:
return new SqlStorage( return new SqlStorage(
this.plugin, this.plugin,
new H2ConnectionFactory(this.plugin, this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2")), new H2ConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2")),
this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX)
); );
case POSTGRESQL: case POSTGRESQL:

View File

@ -25,7 +25,6 @@
package me.lucko.luckperms.common.storage.implementation.sql.connection.file; package me.lucko.luckperms.common.storage.implementation.sql.connection.file;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -34,28 +33,74 @@ import net.kyori.adventure.text.format.NamedTextColor;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
/**
* Abstract {@link ConnectionFactory} using a file based database driver.
*/
abstract class FlatfileConnectionFactory implements ConnectionFactory { abstract class FlatfileConnectionFactory implements ConnectionFactory {
protected static final DecimalFormat DF = new DecimalFormat("#.##"); /** Format used for formatting database file size. */
protected static final DecimalFormat FILE_SIZE_FORMAT = new DecimalFormat("#.##");
protected final Path file; /** The current open connection, if any */
private NonClosableConnection connection;
/** The path to the database file */
private final Path file;
FlatfileConnectionFactory(Path file) { FlatfileConnectionFactory(Path file) {
this.file = file; this.file = file;
} }
@Override /**
public void init(LuckPermsPlugin plugin) { * Creates a connection to the database.
*
* @param file the database file
* @return the connection
* @throws SQLException if any error occurs
*/
protected abstract Connection createConnection(Path file) throws SQLException;
@Override
public synchronized Connection getConnection() throws SQLException {
NonClosableConnection connection = this.connection;
if (connection == null || connection.isClosed()) {
connection = new NonClosableConnection(createConnection(this.file));
this.connection = connection;
}
return connection;
} }
@Override
public void shutdown() throws Exception {
if (this.connection != null) {
this.connection.shutdown();
}
}
/**
* Gets the path of the file the database driver actually ends up writing to.
*
* @return the write file
*/
protected Path getWriteFile() { protected Path getWriteFile() {
return this.file; return this.file;
} }
protected void migrateOldDatabaseFile(String oldName) {
Path oldFile = getWriteFile().getParent().resolve(oldName);
if (Files.exists(oldFile)) {
try {
Files.move(oldFile, getWriteFile());
} catch (IOException e) {
// ignore
}
}
}
@Override @Override
public Map<Component, Component> getMeta() { public Map<Component, Component> getMeta() {
String fileSize; String fileSize;
@ -69,7 +114,7 @@ abstract class FlatfileConnectionFactory implements ConnectionFactory {
} }
double size = length / 1048576D; double size = length / 1048576D;
fileSize = DF.format(size) + "MB"; fileSize = FILE_SIZE_FORMAT.format(size) + "MB";
} else { } else {
fileSize = "0MB"; fileSize = "0MB";
} }

View File

@ -29,47 +29,19 @@ import me.lucko.luckperms.common.dependencies.Dependency;
import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import java.io.IOException; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.sql.Connection; import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Properties; import java.util.Properties;
import java.util.function.Function; import java.util.function.Function;
public class H2ConnectionFactory extends FlatfileConnectionFactory { public class H2ConnectionFactory extends FlatfileConnectionFactory {
private Constructor<?> connectionConstructor;
// the driver used to obtain connections public H2ConnectionFactory(Path file) {
private final Driver driver;
// the active connection
private NonClosableConnection connection;
public H2ConnectionFactory(LuckPermsPlugin plugin, Path file) {
super(file); super(file);
// backwards compat
Path data = file.getParent().resolve("luckperms.db.mv.db");
if (Files.exists(data)) {
try {
Files.move(data, getWriteFile());
} catch (IOException e) {
plugin.getLogger().warn("Unable to move old database", e);
}
}
// setup the classloader
IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER));
try {
Class<?> driverClass = classLoader.loadClass("org.h2.Driver");
Method loadMethod = driverClass.getMethod("load");
this.driver = (Driver) loadMethod.invoke(null);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
@ -78,36 +50,39 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory {
} }
@Override @Override
public synchronized Connection getConnection() throws SQLException { public void init(LuckPermsPlugin plugin) {
if (this.connection == null || this.connection.isClosed()) { migrateOldDatabaseFile("luckperms.db.mv.db");
Connection connection = this.driver.connect("jdbc:h2:" + this.file.toString(), new Properties());
if (connection != null) {
this.connection = NonClosableConnection.wrap(connection);
}
}
if (this.connection == null) { IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER));
throw new SQLException("Unable to get a connection."); try {
Class<?> connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection");
this.connectionConstructor = connectionClass.getConstructor(String.class, Properties.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
} }
return this.connection;
} }
@Override @Override
public void shutdown() throws Exception { protected Connection createConnection(Path file) throws SQLException {
if (this.connection != null) { try {
this.connection.shutdown(); return (Connection) this.connectionConstructor.newInstance("jdbc:h2:" + file.toString(), new Properties());
} catch (ReflectiveOperationException e) {
if (e.getCause() instanceof SQLException) {
throw ((SQLException) e.getCause());
} }
throw new RuntimeException(e);
}
}
@Override
protected Path getWriteFile() {
// h2 appends '.mv.db' to the end of the database name
Path writeFile = super.getWriteFile();
return writeFile.getParent().resolve(writeFile.getFileName().toString() + ".mv.db");
} }
@Override @Override
public Function<String, String> getStatementProcessor() { public Function<String, String> getStatementProcessor() {
return s -> s.replace("'", "`"); return s -> s.replace("'", "`");
} }
@Override
protected Path getWriteFile() {
// h2 appends this to the end of the database file
return super.file.getParent().resolve(super.file.getFileName().toString() + ".mv.db");
}
} }

View File

@ -25,63 +25,32 @@
package me.lucko.luckperms.common.storage.implementation.sql.connection.file; package me.lucko.luckperms.common.storage.implementation.sql.connection.file;
import net.bytebuddy.ByteBuddy; import java.sql.Array;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; import java.sql.Blob;
import net.bytebuddy.implementation.MethodDelegation; import java.sql.CallableStatement;
import java.sql.Clob;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.SQLWarning;
import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; import java.sql.SQLXML;
import static net.bytebuddy.matcher.ElementMatchers.isFinal; import java.sql.Savepoint;
import static net.bytebuddy.matcher.ElementMatchers.not; import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
/** /**
* A wrapper around a {@link Connection} which blocks usage of the default {@link #close()} method. * A wrapper around a {@link Connection} which blocks usage of the default {@link #close()} method.
*/ */
public abstract class NonClosableConnection implements Connection { public class NonClosableConnection implements Connection {
private final Connection delegate;
private static final MethodHandle CONSTRUCTOR; public NonClosableConnection(Connection delegate) {
static {
// construct an implementation of NonClosableConnection
Class<? extends NonClosableConnection> implClass = new ByteBuddy()
.subclass(NonClosableConnection.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
.name(NonClosableConnection.class.getName() + "Impl")
.method(not(isFinal()).and(not(isDeclaredBy(Object.class))))
.intercept(MethodDelegation.toField("delegate"))
.make()
.load(NonClosableConnection.class.getClassLoader())
.getLoaded();
try {
CONSTRUCTOR = MethodHandles.publicLookup().in(implClass)
.findConstructor(implClass, MethodType.methodType(void.class, Connection.class))
.asType(MethodType.methodType(NonClosableConnection.class, Connection.class));
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* Creates a {@link NonClosableConnection} that delegates calls to the given {@link Connection}.
*
* @param connection the connection to wrap
* @return a non closable connection
*/
static NonClosableConnection wrap(Connection connection) {
try {
return (NonClosableConnection) CONSTRUCTOR.invokeExact(connection);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
protected final Connection delegate;
protected NonClosableConnection(Connection delegate) {
this.delegate = delegate; this.delegate = delegate;
} }
@ -110,4 +79,58 @@ public abstract class NonClosableConnection implements Connection {
} }
return this.delegate.unwrap(iface); return this.delegate.unwrap(iface);
} }
// Forward to the delegate connection
@Override public Statement createStatement() throws SQLException { return this.delegate.createStatement(); }
@Override public PreparedStatement prepareStatement(String sql) throws SQLException { return this.delegate.prepareStatement(sql); }
@Override public CallableStatement prepareCall(String sql) throws SQLException { return this.delegate.prepareCall(sql); }
@Override public String nativeSQL(String sql) throws SQLException { return this.delegate.nativeSQL(sql); }
@Override public void setAutoCommit(boolean autoCommit) throws SQLException { this.delegate.setAutoCommit(autoCommit); }
@Override public boolean getAutoCommit() throws SQLException { return this.delegate.getAutoCommit(); }
@Override public void commit() throws SQLException { this.delegate.commit(); }
@Override public void rollback() throws SQLException { this.delegate.rollback(); }
@Override public boolean isClosed() throws SQLException { return this.delegate.isClosed(); }
@Override public DatabaseMetaData getMetaData() throws SQLException { return this.delegate.getMetaData(); }
@Override public void setReadOnly(boolean readOnly) throws SQLException { this.delegate.setReadOnly(readOnly); }
@Override public boolean isReadOnly() throws SQLException { return this.delegate.isReadOnly(); }
@Override public void setCatalog(String catalog) throws SQLException { this.delegate.setCatalog(catalog); }
@Override public String getCatalog() throws SQLException { return this.delegate.getCatalog(); }
@Override public void setTransactionIsolation(int level) throws SQLException { this.delegate.setTransactionIsolation(level); }
@Override public int getTransactionIsolation() throws SQLException { return this.delegate.getTransactionIsolation(); }
@Override public SQLWarning getWarnings() throws SQLException { return this.delegate.getWarnings(); }
@Override public void clearWarnings() throws SQLException { this.delegate.clearWarnings(); }
@Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { return this.delegate.createStatement(resultSetType, resultSetConcurrency); }
@Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); }
@Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency); }
@Override public Map<String, Class<?>> getTypeMap() throws SQLException { return this.delegate.getTypeMap(); }
@Override public void setTypeMap(Map<String, Class<?>> map) throws SQLException { this.delegate.setTypeMap(map); }
@Override public void setHoldability(int holdability) throws SQLException { this.delegate.setHoldability(holdability); }
@Override public int getHoldability() throws SQLException { return this.delegate.getHoldability(); }
@Override public Savepoint setSavepoint() throws SQLException { return this.delegate.setSavepoint(); }
@Override public Savepoint setSavepoint(String name) throws SQLException { return this.delegate.setSavepoint(name); }
@Override public void rollback(Savepoint savepoint) throws SQLException { this.delegate.rollback(savepoint); }
@Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { this.delegate.releaseSavepoint(savepoint); }
@Override public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return this.delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); }
@Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); }
@Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); }
@Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { return this.delegate.prepareStatement(sql, autoGeneratedKeys); }
@Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { return this.delegate.prepareStatement(sql, columnIndexes); }
@Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { return this.delegate.prepareStatement(sql, columnNames); }
@Override public Clob createClob() throws SQLException { return this.delegate.createClob(); }
@Override public Blob createBlob() throws SQLException { return this.delegate.createBlob(); }
@Override public NClob createNClob() throws SQLException { return this.delegate.createNClob(); }
@Override public SQLXML createSQLXML() throws SQLException { return this.delegate.createSQLXML(); }
@Override public boolean isValid(int timeout) throws SQLException { return this.delegate.isValid(timeout); }
@Override public void setClientInfo(String name, String value) throws SQLClientInfoException { this.delegate.setClientInfo(name, value); }
@Override public void setClientInfo(Properties properties) throws SQLClientInfoException { this.delegate.setClientInfo(properties); }
@Override public String getClientInfo(String name) throws SQLException { return this.delegate.getClientInfo(name); }
@Override public Properties getClientInfo() throws SQLException { return this.delegate.getClientInfo(); }
@Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { return this.delegate.createArrayOf(typeName, elements); }
@Override public Struct createStruct(String typeName, Object[] attributes) throws SQLException { return this.delegate.createStruct(typeName, attributes); }
@Override public void setSchema(String schema) throws SQLException { this.delegate.setSchema(schema); }
@Override public String getSchema() throws SQLException { return this.delegate.getSchema(); }
@Override public void abort(Executor executor) throws SQLException { this.delegate.abort(executor); }
@Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { this.delegate.setNetworkTimeout(executor, milliseconds); }
@Override public int getNetworkTimeout() throws SQLException { return this.delegate.getNetworkTimeout(); }
} }

View File

@ -29,10 +29,7 @@ import me.lucko.luckperms.common.dependencies.Dependency;
import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import java.io.IOException; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -40,34 +37,11 @@ import java.util.EnumSet;
import java.util.Properties; import java.util.Properties;
import java.util.function.Function; import java.util.function.Function;
public class SQLiteConnectionFactory extends FlatfileConnectionFactory { public class SqliteConnectionFactory extends FlatfileConnectionFactory {
private Constructor<?> connectionConstructor;
// the method invoked to obtain new connection instances public SqliteConnectionFactory(Path file) {
private final Method createConnectionMethod;
// the active connection
private NonClosableConnection connection;
public SQLiteConnectionFactory(LuckPermsPlugin plugin, Path file) {
super(file); super(file);
// backwards compat
Path data = file.getParent().resolve("luckperms.sqlite");
if (Files.exists(data)) {
try {
Files.move(data, file);
} catch (IOException e) {
plugin.getLogger().warn("Unable to move old database", e);
}
}
// setup the classloader
IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER));
try {
Class<?> jdcbClass = classLoader.loadClass("org.sqlite.JDBC");
this.createConnectionMethod = jdcbClass.getMethod("createConnection", String.class, Properties.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
@ -75,12 +49,24 @@ public class SQLiteConnectionFactory extends FlatfileConnectionFactory {
return "SQLite"; return "SQLite";
} }
private Connection createConnection(String url) throws SQLException { @Override
public void init(LuckPermsPlugin plugin) {
migrateOldDatabaseFile("luckperms.sqlite");
IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER));
try { try {
return (Connection) this.createConnectionMethod.invoke(null, url, new Properties()); Class<?> connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection");
} catch (IllegalAccessException e) { this.connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (InvocationTargetException e) { }
}
@Override
protected Connection createConnection(Path file) throws SQLException {
try {
return (Connection) this.connectionConstructor.newInstance("jdbc:sqlite:" + file.toString(), file.toString(), new Properties());
} catch (ReflectiveOperationException e) {
if (e.getCause() instanceof SQLException) { if (e.getCause() instanceof SQLException) {
throw ((SQLException) e.getCause()); throw ((SQLException) e.getCause());
} }
@ -88,32 +74,8 @@ public class SQLiteConnectionFactory extends FlatfileConnectionFactory {
} }
} }
@Override
public synchronized Connection getConnection() throws SQLException {
if (this.connection == null || this.connection.isClosed()) {
Connection connection = createConnection("jdbc:sqlite:" + this.file.toString());
if (connection != null) {
this.connection = NonClosableConnection.wrap(connection);
}
}
if (this.connection == null) {
throw new SQLException("Unable to get a connection.");
}
return this.connection;
}
@Override
public void shutdown() throws Exception {
if (this.connection != null) {
this.connection.shutdown();
}
}
@Override @Override
public Function<String, String> getStatementProcessor() { public Function<String, String> getStatementProcessor() {
return s -> s.replace("'", "`"); return s -> s.replace("'", "`");
} }
} }