diff --git a/ci/configurations/alpine-temurin21-mysql.compose.yml b/ci/configurations/alpine-temurin21-mysql.compose.yml index d8416a08c..0ca0c7eaf 100644 --- a/ci/configurations/alpine-temurin21-mysql.compose.yml +++ b/ci/configurations/alpine-temurin21-mysql.compose.yml @@ -22,7 +22,7 @@ services: condition: service_healthy environment: DATABASE: mysql - DATABASE_URL: jdbc:mysql://db:3306/mirthdb + DATABASE_URL: jdbc:mysql://db:3306/mirthdb?sessionVariables=transaction_isolation='READ-COMMITTED' DATABASE_USERNAME: mirthdb DATABASE_PASSWORD: mirthdb DATABASE_MAX_CONNECTIONS: 20 diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoFactory.java index be702d2c8..51fff7e9d 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoFactory.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoFactory.java @@ -159,6 +159,11 @@ public JdbcDao getDao(SerializerProvider serializerProvider) { return getDao(donkey, connection, querySource, statementSource, serializerProvider, encryptMessageContent, encryptAttachments, encryptCustomMetaData, decryptData, statisticsUpdater, channelController.getStatistics(), channelController.getTotalStatistics(), statsServerId); } + /** Assert a brand new database and server is acceptable */ + public void checkNewDatabase(Connection connection) { + // By default, all new databases are considered acceptable. + } + protected JdbcDao getDao(Donkey donkey, Connection connection, QuerySource querySource, PreparedStatementSource statementSource, SerializerProvider serializerProvider, boolean encryptMessageContent, boolean encryptAttachments, boolean encryptCustomMetaData, boolean decryptData, StatisticsUpdater statisticsUpdater, Statistics currentStats, Statistics totalStats, String statsServerId) { return new JdbcDao(donkey, connection, querySource, statementSource, serializerProvider, encryptMessageContent, encryptAttachments, encryptCustomMetaData, decryptData, statisticsUpdater, currentStats, totalStats, statsServerId); } diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java index ab08ebf45..bbae54444 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java @@ -9,13 +9,55 @@ package com.mirth.connect.donkey.server.data.jdbc; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.mirth.connect.donkey.server.data.DonkeyDaoException; import com.mirth.connect.donkey.util.SerializerProvider; public class MysqlDaoFactory extends JdbcDaoFactory { + private static final Logger logger = LogManager.getLogger(MysqlDaoFactory.class); + private final AtomicBoolean hasCheckedTransactionIsolation = new AtomicBoolean(false); + @Override public JdbcDao getDao(SerializerProvider serializerProvider) { JdbcDao dao = super.getDao(serializerProvider); dao.setQuoteChar('`'); + checkTransactionIsolation(dao); return dao; } + + @Override + public void checkNewDatabase(Connection connection) { + try { + if (connection.getTransactionIsolation() == Connection.TRANSACTION_REPEATABLE_READ) { + throw new DonkeyDaoException("Creating a new database with REPEATABLE-READ transaction " + + "isolation is not allowed. Set the transaction isolation level in the database " + + "URL or the database server to READ-COMMITTED."); + } + } catch (SQLException e) { + throw new DonkeyDaoException("Failed to check the transaction isolation level for this MySQL connection.", e); + } + } + + /** + * Warns once if the connection is using the default REPEATABLE-READ transaction isolation level. + */ + private void checkTransactionIsolation(JdbcDao dao) { + if (!hasCheckedTransactionIsolation.compareAndSet(false, true)) { + return; + } + + try { + if (dao.getConnection().getTransactionIsolation() == Connection.TRANSACTION_REPEATABLE_READ) { + logger.error("This MySQL connection is using the REPEATABLE-READ transaction isolation level. The MySQL REPEATABLE-READ will cause intermittent issues. Set the transaction isolation level in the database URL or the database server to READ-COMMITTED to avoid these problems."); + } + } catch (SQLException e) { + logger.error("Failed to check the transaction isolation level for this MySQL connection.", e); + } + } } diff --git a/server/conf/mirth.properties b/server/conf/mirth.properties index 4c9894277..44286328c 100644 --- a/server/conf/mirth.properties +++ b/server/conf/mirth.properties @@ -80,7 +80,7 @@ database = derby # examples: # Derby jdbc:derby:${dir.appdata}/mirthdb;create=true # PostgreSQL jdbc:postgresql://localhost:5432/mirthdb -# MySQL jdbc:mysql://localhost:3306/mirthdb +# MySQL jdbc:mysql://localhost:3306/mirthdb?sessionVariables=transaction_isolation='READ-COMMITTED' # Oracle jdbc:oracle:thin:@localhost:1521:DB # SQL Server/Sybase (jTDS) jdbc:jtds:sqlserver://localhost:1433/mirthdb # Microsoft SQL Server jdbc:sqlserver://localhost:1433;databaseName=mirthdb diff --git a/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java b/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java index efb77503f..bbbea9f53 100644 --- a/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java +++ b/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java @@ -33,6 +33,8 @@ import com.mirth.connect.client.core.PropertiesConfigurationUtil; import com.mirth.connect.client.core.Version; +import com.mirth.connect.donkey.server.data.DonkeyDaoException; +import com.mirth.connect.donkey.server.data.jdbc.JdbcDaoFactory; import com.mirth.connect.model.Channel; import com.mirth.connect.model.ExportClearable; import com.mirth.connect.model.alert.AlertModel; @@ -258,6 +260,14 @@ private Migrator getMigrator(Version version) { private void initDatabase(Connection connection) throws MigrationException { // If missing this table we can assume that they don't have the schema installed if (!DatabaseUtil.tableExists(connection, "CONFIGURATION")) { + // Since we are creating a new database, let the database-specific DAO + // factory check if it is acceptable. + try { + JdbcDaoFactory.getInstance(getDatabaseType()).checkNewDatabase(connection); + } catch (DonkeyDaoException e) { + throw new MigrationException(e.getMessage(), e); + } + executeScript("/" + getDatabaseType() + "/" + getDatabaseType() + "-database.sql"); initializeAdminPassword();