3091/mariadb driver support (#3122)

* Fix MariaDB 11 driver issue
* Update some more logging messages
* Throw DBInitException if using MariaDB 11.0.2
* Fix mariadb container health check
* Use 11.1-rc for mariadb image

Affects issues:
- Fixed #3091 for MariaDB 11.1.1 or newer
This commit is contained in:
Aurora Lahtela 2023-07-20 09:01:26 +03:00 committed by GitHub
parent ebadb080b2
commit 181310dc7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 108 additions and 18 deletions

View File

@ -11,7 +11,7 @@ jobs:
services:
mariadb:
image: mariadb:10.6.14
image: mariadb:11.1-rc
ports:
- 3306
env:
@ -19,7 +19,7 @@ jobs:
MYSQL_PASSWORD: password
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: password
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: 📥 Checkout git repository

View File

@ -88,6 +88,7 @@ subprojects {
jettyVersion = "11.0.15"
caffeineVersion = "2.9.2"
mysqlVersion = "8.0.33"
mariadbVersion = "3.1.4"
sqliteVersion = "3.41.2.1"
adventureVersion = "4.14.0"
hikariVersion = "5.0.1"
@ -134,8 +135,9 @@ subprojects {
// Awaitility (Concurrent wait conditions)
// Testing dependencies required by Plan
testImplementation "org.xerial:sqlite-jdbc:$sqliteVersion" // SQLite
testImplementation "mysql:mysql-connector-java:$mysqlVersion" // MySQL
testImplementation "org.xerial:sqlite-jdbc:$sqliteVersion" // SQLite
testImplementation "com.mysql:mysql-connector-j:$mysqlVersion" // MySQL
testImplementation "org.mariadb.jdbc:mariadb-java-client:$mariadbVersion" // MariaDB
}
configurations {

View File

@ -10,10 +10,11 @@ plugins {
configurations {
// Runtime downloading scopes
mysqlDriver
mariadbDriver
sqliteDriver
ipAddressMatcher
testImplementation.extendsFrom mysqlDriver, sqliteDriver, ipAddressMatcher
compileOnly.extendsFrom mysqlDriver, sqliteDriver, ipAddressMatcher
testImplementation.extendsFrom mysqlDriver, mariadbDriver, sqliteDriver, ipAddressMatcher
compileOnly.extendsFrom mysqlDriver, mariadbDriver, sqliteDriver, ipAddressMatcher
swaggerJson // swagger.json configuration
}
@ -26,6 +27,14 @@ task generateResourceForMySQLDriver(type: GenerateDependencyDownloadResourceTask
includeShadowJarRelocations = false
}
task generateResourceForMariaDBDriver(type: GenerateDependencyDownloadResourceTask) {
var conf = configurations.mariadbDriver
configuration = conf
file = "assets/plan/dependencies/" + conf.name + ".txt"
// Not necessary to include in the resource
includeShadowJarRelocations = false
}
task generateResourceForSQLiteDriver(type: GenerateDependencyDownloadResourceTask) {
var conf = configurations.sqliteDriver
configuration = conf
@ -52,7 +61,8 @@ dependencies {
// Effectively disables relocating
exclude module: "jar-relocator"
}
mysqlDriver "mysql:mysql-connector-java:$mysqlVersion"
mysqlDriver "com.mysql:mysql-connector-j:$mysqlVersion"
mariadbDriver "org.mariadb.jdbc:mariadb-java-client:$mariadbVersion"
sqliteDriver "org.xerial:sqlite-jdbc:$sqliteVersion"
ipAddressMatcher "com.github.seancfoley:ipaddress:$ipAddressMatcherVersion"

View File

@ -0,0 +1,29 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.exceptions.database;
/**
* Exception thrown when MySQL driver can't connect to MariaDB.
*
* @author AuroraLS3
*/
public class MariaDB11Exception extends DBInitException {
public MariaDB11Exception(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -18,11 +18,14 @@ package com.djrapitops.plan.storage.database;
import com.djrapitops.plan.exceptions.database.DBInitException;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.exceptions.database.MariaDB11Exception;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DatabaseSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.storage.database.queries.schema.MySQLSchemaQueries;
import com.djrapitops.plan.storage.database.transactions.init.OperationCriticalTransaction;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
@ -50,6 +53,8 @@ public class MySQLDB extends SQLDB {
private static int increment = 1;
private static boolean useMariaDbDriver = false;
protected HikariDataSource dataSource;
@Inject
@ -77,9 +82,10 @@ public class MySQLDB extends SQLDB {
@Override
protected List<String> getDependencyResource() {
try {
return files.getResourceFromJar("dependencies/mysqlDriver.txt").asLines();
String driverFile = useMariaDbDriver ? "dependencies/mariadbDriver.txt" : "dependencies/mysqlDriver.txt";
return files.getResourceFromJar(driverFile).asLines();
} catch (IOException e) {
throw new DBInitException("Failed to get MySQL dependency information", e);
throw new DBInitException("Failed to get " + (useMariaDbDriver ? "MariaDB" : "MySQL") + " dependency information", e);
}
}
@ -89,7 +95,7 @@ public class MySQLDB extends SQLDB {
@Override
public void setupDataSource() {
if (driverClassLoader == null) {
logger.info("Downloading MySQL Driver, this may take a while...");
logger.info("Downloading " + (useMariaDbDriver ? "MariaDB" : "MySQL") + " Driver, this may take a while...");
downloadDriver();
}
@ -99,6 +105,21 @@ public class MySQLDB extends SQLDB {
// Set the context class loader to the driver class loader for Hikari to use for finding the Driver
currentThread.setContextClassLoader(driverClassLoader);
try {
loadDataSource();
} catch (MariaDB11Exception e) {
// Try to set up again using MariaDB driver
driverClassLoader = null;
dataSource = null;
useMariaDbDriver = true;
loadDataSource();
}
// Reset the context classloader back to what it was originally set to, now that the DataSource is created
currentThread.setContextClassLoader(previousClassLoader);
}
private void loadDataSource() {
try {
HikariConfig hikariConfig = new HikariConfig();
@ -107,12 +128,14 @@ public class MySQLDB extends SQLDB {
String database = config.get(DatabaseSettings.MYSQL_DATABASE);
String launchOptions = config.get(DatabaseSettings.MYSQL_LAUNCH_OPTIONS);
// REGEX: match "?", match "word=word&" *-times, match "word=word"
if (launchOptions.isEmpty() || !launchOptions.matches("\\?((([\\w-])+=.+)&)*(([\\w-])+=.+)")) {
launchOptions = "?rewriteBatchedStatements=true&useSSL=false";
logger.error(locale.getString(PluginLang.DB_MYSQL_LAUNCH_OPTIONS_FAIL, launchOptions));
}
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariConfig.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database + launchOptions);
hikariConfig.setDriverClassName(useMariaDbDriver ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver");
String protocol = useMariaDbDriver ? "jdbc:mariadb" : "jdbc:mysql";
hikariConfig.setJdbcUrl(protocol + "://" + host + ":" + port + "/" + database + launchOptions);
String username = config.get(DatabaseSettings.MYSQL_USER);
String password = config.get(DatabaseSettings.MYSQL_PASS);
@ -127,17 +150,34 @@ public class MySQLDB extends SQLDB {
hikariConfig.setAutoCommit(false);
setMaxConnections(hikariConfig);
hikariConfig.setMaxLifetime(config.get(DatabaseSettings.MAX_LIFETIME));
hikariConfig.setLeakDetectionThreshold(TimeUnit.SECONDS.toMillis(29L));
hikariConfig.setLeakDetectionThreshold(config.get(DatabaseSettings.MAX_LIFETIME) + TimeUnit.SECONDS.toMillis(4L));
this.dataSource = new HikariDataSource(hikariConfig);
} catch (HikariPool.PoolInitializationException e) {
if (e.getMessage().contains("Unknown system variable 'transaction_isolation'")) {
throw new MariaDB11Exception("MySQL driver is incompatible with database that is being used.", e);
}
throw new DBInitException("Failed to set-up HikariCP Datasource: " + e.getMessage(), e);
} finally {
unloadMySQLDriver();
}
// Reset the context classloader back to what it was originally set to, now that the DataSource is created
currentThread.setContextClassLoader(previousClassLoader);
if (useMariaDbDriver) {
checkMariaDBVersionIncompatibility();
}
}
private void checkMariaDBVersionIncompatibility() {
executeTransaction(new OperationCriticalTransaction() {
@Override
protected void performOperations() {
query(MySQLSchemaQueries.getVersion())
.filter("11.0.2-MariaDB"::equals)
.ifPresent(badVersion -> {
throw new DBInitException("MariaDB version " + badVersion + " inserts incorrect data due to a bug in query execution order so it is not supported. Upgrade MariaDB to 11.1.1 or newer, or downgrade to MariaDB 10.");
});
}
});
}
private void setMaxConnections(HikariConfig hikariConfig) {
@ -156,7 +196,8 @@ public class MySQLDB extends SQLDB {
Driver driver = drivers.nextElement();
Class<?> driverClass = driver.getClass();
// Checks that it's from our class loader to avoid unloading another plugin's/the server's driver
if ("com.mysql.cj.jdbc.Driver".equals(driverClass.getName()) && driverClass.getClassLoader() == driverClassLoader) {
String driverName = useMariaDbDriver ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
if (driverName.equals(driverClass.getName()) && driverClass.getClassLoader() == driverClassLoader) {
try {
DriverManager.deregisterDriver(driver);
} catch (SQLException e) {

View File

@ -19,12 +19,14 @@ package com.djrapitops.plan.storage.database.queries.schema;
import com.djrapitops.plan.storage.database.queries.HasMoreThanZeroQueryStatement;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryStatement;
import org.intellij.lang.annotations.Language;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
@ -39,6 +41,12 @@ public class MySQLSchemaQueries {
/* Static method class */
}
public static Query<Optional<String>> getVersion() {
@Language("MySQL")
String sql = "SELECT VERSION()";
return db -> db.queryOptional(sql, row -> row.getString(1));
}
public static Query<Boolean> doesTableExist(String tableName) {
String sql = SELECT + "COUNT(1) as c FROM information_schema.TABLES WHERE table_name=? AND TABLE_SCHEMA=DATABASE()";
return new HasMoreThanZeroQueryStatement(sql) {

View File

@ -50,8 +50,8 @@ public class WorldTable {
public static final String SELECT_WORLD_ID_STATEMENT = '(' +
SELECT + TABLE_NAME + '.' + ID + FROM + TABLE_NAME +
WHERE + '(' + NAME + "=?)" +
AND + '(' + TABLE_NAME + '.' + SERVER_UUID + "=?)" +
WHERE + NAME + "=?" +
AND + TABLE_NAME + '.' + SERVER_UUID + "=?" +
" LIMIT 1)";
private WorldTable() {