From d898f894137413a058195a600d86ae6be6ee4a7d Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Mon, 21 Sep 2026 16:44:05 -0500 Subject: [PATCH 1/3] Add comment and warning for MySQL repeatable read Signed-off-by: Mitch Gaffigan --- .../alpine-temurin21-mysql.compose.yml | 2 +- .../server/data/jdbc/MysqlDaoFactory.java | 26 +++++++++++++++++++ server/conf/mirth.properties | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/ci/configurations/alpine-temurin21-mysql.compose.yml b/ci/configurations/alpine-temurin21-mysql.compose.yml index d8416a08cb..0ca0c7eafc 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/MysqlDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java index ab08ebf458..05f6d34346 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,39 @@ package com.mirth.connect.donkey.server.data.jdbc; +import java.sql.Connection; +import java.sql.SQLException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import com.mirth.connect.donkey.util.SerializerProvider; public class MysqlDaoFactory extends JdbcDaoFactory { + private Logger logger = LogManager.getLogger(this.getClass()); + private boolean hasCheckedTransactionIsolation = false; + @Override public JdbcDao getDao(SerializerProvider serializerProvider) { JdbcDao dao = super.getDao(serializerProvider); dao.setQuoteChar('`'); + checkTransactionIsolation(dao); return dao; } + + /** + * Warns once if the connection is using the default REPEATABLE-READ transaction isolation level. + */ + private void checkTransactionIsolation(JdbcDao dao) { + if (hasCheckedTransactionIsolation) return; + hasCheckedTransactionIsolation = true; + + try { + if (dao.getConnection().getTransactionIsolation() == Connection.TRANSACTION_REPEATABLE_READ) { + logger.error("This MySQL connection is not using the READ-COMMITTED 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 4c98942771..44286328c5 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 From 4c14bb49cc7144148b2a36fbc5997513b8c0319d Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Mon, 26 Oct 2026 18:06:58 -0500 Subject: [PATCH 2/3] Block new mysql databases at wrong transaction isolation level Signed-off-by: Mitch Gaffigan --- .../donkey/server/data/jdbc/JdbcDaoFactory.java | 5 +++++ .../donkey/server/data/jdbc/MysqlDaoFactory.java | 14 ++++++++++++++ .../connect/server/migration/ServerMigrator.java | 10 ++++++++++ 3 files changed, 29 insertions(+) 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 be702d2c82..51fff7e9d4 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 05f6d34346..4bd18f79e8 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 @@ -15,6 +15,7 @@ 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 { @@ -29,6 +30,19 @@ public JdbcDao getDao(SerializerProvider serializerProvider) { 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. */ 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 efb77503f8..bbbea9f535 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(); From de18cb3c29231d8c51bbdfceb10c580652a0bc8e Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Wed, 28 Oct 2026 10:34:49 -0500 Subject: [PATCH 3/3] Apply batched suggestions from code review Co-authored-by: Tony Germano <39179051+tonygermano@users.noreply.github.com> Signed-off-by: Mitch Gaffigan --- .../donkey/server/data/jdbc/MysqlDaoFactory.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 4bd18f79e8..bbae544440 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 @@ -11,6 +11,7 @@ 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; @@ -19,8 +20,8 @@ import com.mirth.connect.donkey.util.SerializerProvider; public class MysqlDaoFactory extends JdbcDaoFactory { - private Logger logger = LogManager.getLogger(this.getClass()); - private boolean hasCheckedTransactionIsolation = false; + private static final Logger logger = LogManager.getLogger(MysqlDaoFactory.class); + private final AtomicBoolean hasCheckedTransactionIsolation = new AtomicBoolean(false); @Override public JdbcDao getDao(SerializerProvider serializerProvider) { @@ -47,12 +48,13 @@ public void checkNewDatabase(Connection connection) { * Warns once if the connection is using the default REPEATABLE-READ transaction isolation level. */ private void checkTransactionIsolation(JdbcDao dao) { - if (hasCheckedTransactionIsolation) return; - hasCheckedTransactionIsolation = true; + if (!hasCheckedTransactionIsolation.compareAndSet(false, true)) { + return; + } try { if (dao.getConnection().getTransactionIsolation() == Connection.TRANSACTION_REPEATABLE_READ) { - logger.error("This MySQL connection is not using the READ-COMMITTED 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."); + 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);