Limit parallelism to one thread for SQLite

Fixes #596
This commit is contained in:
games647
2021-08-14 17:54:57 +02:00
parent f8fe3d7d71
commit 7dd0aa5bca
6 changed files with 207 additions and 103 deletions

View File

@ -27,12 +27,14 @@ package com.github.games647.fastlogin.core.shared;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.storage.MySQLStorage;
import com.github.games647.fastlogin.core.storage.SQLStorage;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
import com.github.games647.fastlogin.core.storage.SQLiteStorage;
import com.google.common.net.HostAndPort;
import com.zaxxer.hikari.HikariConfig;
@ -82,7 +84,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
private final MojangResolver resolver = new MojangResolver();
private Configuration config;
private AuthStorage storage;
private SQLStorage storage;
private RateLimiter rateLimiter;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
@ -169,7 +171,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
return resolver;
}
public AuthStorage getStorage() {
public SQLStorage getStorage() {
return storage;
}
@ -189,26 +191,31 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
}
public boolean setupDatabase() {
if (!checkDriver(config.getString("driver"))) {
String driver = config.getString("driver");
if (!checkDriver(driver)) {
return false;
}
HikariConfig databaseConfig = new HikariConfig();
databaseConfig.setDriverClassName(config.getString("driver"));
databaseConfig.setDriverClassName(driver);
String host = config.get("host", "");
int port = config.get("port", 3306);
String database = config.getString("database");
boolean useSSL = config.get("useSSL", false);
databaseConfig.setUsername(config.get("username", ""));
databaseConfig.setPassword(config.getString("password"));
databaseConfig.setConnectionTimeout(config.getInt("timeout", 30) * 1_000L);
databaseConfig.setMaxLifetime(config.getInt("lifetime", 30) * 1_000L);
storage = new AuthStorage(this, host, port, database, databaseConfig, useSSL);
if (driver.contains("sqlite")) {
storage = new SQLiteStorage(this, database, databaseConfig);
} else {
String host = config.get("host", "");
int port = config.get("port", 3306);
boolean useSSL = config.get("useSSL", false);
databaseConfig.setUsername(config.get("username", ""));
databaseConfig.setPassword(config.getString("password"));
storage = new MySQLStorage(this, host, port, database, databaseConfig, useSSL);
}
try {
storage.createTables();
return true;

View File

@ -25,7 +25,7 @@
*/
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.storage.SQLStorage;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
@ -55,7 +55,7 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
return;
}
AuthStorage storage = core.getStorage();
SQLStorage storage = core.getStorage();
StoredProfile playerProfile = session.getProfile();
try {
if (isOnlineMode()) {

View File

@ -0,0 +1,15 @@
package com.github.games647.fastlogin.core.storage;
import com.github.games647.fastlogin.core.StoredProfile;
import java.util.UUID;
public interface AuthStorage {
StoredProfile loadProfile(String name);
StoredProfile loadProfile(UUID uuid);
void save(StoredProfile playerProfile);
void close();
}

View File

@ -0,0 +1,59 @@
package com.github.games647.fastlogin.core.storage;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.zaxxer.hikari.HikariConfig;
public class MySQLStorage extends SQLStorage {
public MySQLStorage(FastLoginCore<?, ?, ?> core, String host, int port, String database, HikariConfig config, boolean useSSL) {
super(core,
"mysql://" + host + ':' + port + '/' + database,
setParams(config, useSSL));
}
private static HikariConfig setParams(HikariConfig config, boolean useSSL) {
// Require SSL on the server if requested in config - this will also verify certificate
// Those values are deprecated in favor of sslMode
config.addDataSourceProperty("useSSL", useSSL);
config.addDataSourceProperty("requireSSL", useSSL);
// prefer encrypted if possible
config.addDataSourceProperty("sslMode", "PREFERRED");
// adding paranoid hides hostname, username, version and so
// could be useful for hiding server details
config.addDataSourceProperty("paranoid", true);
// enable MySQL specific optimizations
// disabled by default - will return the same prepared statement instance
config.addDataSourceProperty("cachePrepStmts", true);
// default prepStmtCacheSize 25 - amount of cached statements
config.addDataSourceProperty("prepStmtCacheSize", 250);
// default prepStmtCacheSqlLimit 256 - length of SQL
config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
// default false - available in newer versions caches the statements server-side
config.addDataSourceProperty("useServerPrepStmts", true);
// default false - prefer use of local values for autocommit and
// transaction isolation (alwaysSendSetIsolation) should only be enabled if always use the set* methods
// instead of raw SQL
// https://forums.mysql.com/read.php?39,626495,626512
config.addDataSourceProperty("useLocalSessionState", true);
// rewrite batched statements to a single statement, adding them behind each other
// only useful for addBatch statements and inserts
config.addDataSourceProperty("rewriteBatchedStatements", true);
// cache result metadata
config.addDataSourceProperty("cacheResultSetMetadata", true);
// cache results of show variables and collation per URL
config.addDataSourceProperty("cacheServerConfiguration", true);
// default false - set auto commit only if not matching
config.addDataSourceProperty("elideSetAutoCommits", true);
// default true - internal timers for idle calculation -> removes System.getCurrentTimeMillis call per query
// Some platforms are slow on this and it could affect the throughput about 3% according to MySQL
// performance gems presentation
// In our case it can be useful to see the time in error messages
// config.addDataSourceProperty("maintainTimeStats", false);
return config;
}
}

View File

@ -23,9 +23,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.games647.fastlogin.core;
package com.github.games647.fastlogin.core.storage;
import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@ -42,23 +43,34 @@ import java.util.concurrent.ThreadFactory;
import static java.sql.Statement.RETURN_GENERATED_KEYS;
public class AuthStorage {
public abstract class SQLStorage implements AuthStorage {
private static final String PREMIUM_TABLE = "premium";
private static final String JDBC_PROTOCOL = "jdbc:";
private static final String LOAD_BY_NAME = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `Name`=? LIMIT 1";
private static final String LOAD_BY_UUID = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `UUID`=? LIMIT 1";
private static final String INSERT_PROFILE = "INSERT INTO `" + PREMIUM_TABLE
protected static final String PREMIUM_TABLE = "premium";
protected static final String CREATE_TABLE_STMT = "CREATE TABLE IF NOT EXISTS `" + PREMIUM_TABLE + "` ("
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
+ "`UUID` CHAR(36), "
+ "`Name` VARCHAR(16) NOT NULL, "
+ "`Premium` BOOLEAN NOT NULL, "
+ "`LastIp` VARCHAR(255) NOT NULL, "
+ "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
//the premium shouldn't steal the cracked account by changing the name
+ "UNIQUE (`Name`) "
+ ')';
protected static final String LOAD_BY_NAME = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `Name`=? LIMIT 1";
protected static final String LOAD_BY_UUID = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `UUID`=? LIMIT 1";
protected static final String INSERT_PROFILE = "INSERT INTO `" + PREMIUM_TABLE
+ "` (`UUID`, `Name`, `Premium`, `LastIp`) " + "VALUES (?, ?, ?, ?) ";
// limit not necessary here, because it's unique
private static final String UPDATE_PROFILE = "UPDATE `" + PREMIUM_TABLE
protected static final String UPDATE_PROFILE = "UPDATE `" + PREMIUM_TABLE
+ "` SET `UUID`=?, `Name`=?, `Premium`=?, `LastIp`=?, `LastLogin`=CURRENT_TIMESTAMP WHERE `UserID`=?";
private final FastLoginCore<?, ?, ?> core;
private final HikariDataSource dataSource;
protected final FastLoginCore<?, ?, ?> core;
protected final HikariDataSource dataSource;
public AuthStorage(FastLoginCore<?, ?, ?> core, String host, int port, String databasePath,
HikariConfig config, boolean useSSL) {
public SQLStorage(FastLoginCore<?, ?, ?> core, String jdbcURL, HikariConfig config) {
this.core = core;
config.setPoolName(core.getPlugin().getName());
@ -67,68 +79,7 @@ public class AuthStorage {
config.setThreadFactory(platformThreadFactory);
}
String jdbcUrl = "jdbc:";
if (config.getDriverClassName().contains("sqlite")) {
String pluginFolder = core.getPlugin().getPluginFolder().toAbsolutePath().toString();
databasePath = databasePath.replace("{pluginDir}", pluginFolder);
jdbcUrl += "sqlite://" + databasePath;
config.setConnectionTestQuery("SELECT 1");
config.setMaximumPoolSize(1);
//a try to fix https://www.spigotmc.org/threads/fastlogin.101192/page-26#post-1874647
// format strings retrieved by the timestamp column to match them from MySQL
config.addDataSourceProperty("date_string_format", "yyyy-MM-dd HH:mm:ss");
// TODO: test first for compatibility
// config.addDataSourceProperty("date_precision", "seconds");
} else {
jdbcUrl += "mysql://" + host + ':' + port + '/' + databasePath;
// Require SSL on the server if requested in config - this will also verify certificate
// Those values are deprecated in favor of sslMode
config.addDataSourceProperty("useSSL", useSSL);
config.addDataSourceProperty("requireSSL", useSSL);
// prefer encrypted if possible
config.addDataSourceProperty("sslMode", "PREFERRED");
// adding paranoid hides hostname, username, version and so
// could be useful for hiding server details
config.addDataSourceProperty("paranoid", true);
// enable MySQL specific optimizations
// disabled by default - will return the same prepared statement instance
config.addDataSourceProperty("cachePrepStmts", true);
// default prepStmtCacheSize 25 - amount of cached statements
config.addDataSourceProperty("prepStmtCacheSize", 250);
// default prepStmtCacheSqlLimit 256 - length of SQL
config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
// default false - available in newer versions caches the statements server-side
config.addDataSourceProperty("useServerPrepStmts", true);
// default false - prefer use of local values for autocommit and
// transaction isolation (alwaysSendSetIsolation) should only be enabled if always use the set* methods
// instead of raw SQL
// https://forums.mysql.com/read.php?39,626495,626512
config.addDataSourceProperty("useLocalSessionState", true);
// rewrite batched statements to a single statement, adding them behind each other
// only useful for addBatch statements and inserts
config.addDataSourceProperty("rewriteBatchedStatements", true);
// cache result metadata
config.addDataSourceProperty("cacheResultSetMetadata", true);
// cache results of show variables and collation per URL
config.addDataSourceProperty("cacheServerConfiguration", true);
// default false - set auto commit only if not matching
config.addDataSourceProperty("elideSetAutoCommits", true);
// default true - internal timers for idle calculation -> removes System.getCurrentTimeMillis call per query
// Some platforms are slow on this and it could affect the throughput about 3% according to MySQL
// performance gems presentation
// In our case it can be useful to see the time in error messages
// config.addDataSourceProperty("maintainTimeStats", false);
}
config.setJdbcUrl(jdbcUrl);
config.setJdbcUrl(JDBC_PROTOCOL + jdbcURL);
this.dataSource = new HikariDataSource(config);
}
@ -136,28 +87,15 @@ public class AuthStorage {
// choose surrogate PK(ID), because UUID can be null for offline players
// if UUID is always Premium UUID we would have to update offline player entries on insert
// name cannot be PK, because it can be changed for premium players
String createDataStmt = "CREATE TABLE IF NOT EXISTS `" + PREMIUM_TABLE + "` ("
+ "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, "
+ "`UUID` CHAR(36), "
+ "`Name` VARCHAR(16) NOT NULL, "
+ "`Premium` BOOLEAN NOT NULL, "
+ "`LastIp` VARCHAR(255) NOT NULL, "
+ "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
//the premium shouldn't steal the cracked account by changing the name
+ "UNIQUE (`Name`) "
+ ')';
if (dataSource.getJdbcUrl().contains("sqlite")) {
createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT");
}
//todo: add unique uuid index usage
try (Connection con = dataSource.getConnection();
Statement createStmt = con.createStatement()) {
createStmt.executeUpdate(createDataStmt);
createStmt.executeUpdate(CREATE_TABLE_STMT);
}
}
@Override
public StoredProfile loadProfile(String name) {
try (Connection con = dataSource.getConnection();
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_NAME)
@ -174,6 +112,7 @@ public class AuthStorage {
return null;
}
@Override
public StoredProfile loadProfile(UUID uuid) {
try (Connection con = dataSource.getConnection();
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_UUID)) {
@ -205,6 +144,7 @@ public class AuthStorage {
return Optional.empty();
}
@Override
public void save(StoredProfile playerProfile) {
try (Connection con = dataSource.getConnection()) {
String uuid = playerProfile.getOptId().map(UUIDAdapter::toMojangId).orElse(null);
@ -245,6 +185,7 @@ public class AuthStorage {
}
}
@Override
public void close() {
dataSource.close();
}

View File

@ -0,0 +1,82 @@
package com.github.games647.fastlogin.core.storage;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import com.zaxxer.hikari.HikariConfig;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SQLiteStorage extends SQLStorage {
private final Lock lock = new ReentrantLock();
public SQLiteStorage(FastLoginCore<?, ?, ?> core, String databasePath, HikariConfig config) {
super(core,
"sqlite://" + replacePathVariables(core.getPlugin(), databasePath),
setParams(config));
}
private static HikariConfig setParams(HikariConfig config) {
config.setConnectionTestQuery("SELECT 1");
config.setMaximumPoolSize(1);
//a try to fix https://www.spigotmc.org/threads/fastlogin.101192/page-26#post-1874647
// format strings retrieved by the timestamp column to match them from MySQL
config.addDataSourceProperty("date_string_format", "yyyy-MM-dd HH:mm:ss");
// TODO: test first for compatibility
// config.addDataSourceProperty("date_precision", "seconds");
return config;
}
@Override
public StoredProfile loadProfile(String name) {
lock.lock();
try {
return super.loadProfile(name);
} finally {
lock.unlock();
}
}
@Override
public StoredProfile loadProfile(UUID uuid) {
lock.lock();
try {
return super.loadProfile(uuid);
} finally {
lock.unlock();
}
}
@Override
public void save(StoredProfile playerProfile) {
lock.lock();
try {
super.save(playerProfile);
} finally {
lock.unlock();
}
}
@Override
public void createTables() throws SQLException {
try (Connection con = dataSource.getConnection();
Statement createStmt = con.createStatement()) {
// SQLite has a different syntax for auto increment
createStmt.executeUpdate(CREATE_TABLE_STMT.replace("AUTO_INCREMENT", "AUTOINCREMENT"));
}
}
private static String replacePathVariables(PlatformPlugin<?> plugin, String input) {
String pluginFolder = plugin.getPluginFolder().toAbsolutePath().toString();
return input.replace("{pluginDir}", pluginFolder);
}
}