From 1d2bfae6430cea494a7aa0c7c04f3435cb0456c1 Mon Sep 17 00:00:00 2001 From: Andrija Panic <45762285+andrijapanicsb@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:05:32 +0200 Subject: [PATCH] server: make HikariCP leak detection configurable --- client/conf/db.properties.in | 5 +++ .../com/cloud/utils/db/TransactionLegacy.java | 38 ++++++++++++++--- .../cloud/utils/db/TransactionLegacyTest.java | 41 +++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/client/conf/db.properties.in b/client/conf/db.properties.in index 0f7d2706a427..04d18dd67890 100644 --- a/client/conf/db.properties.in +++ b/client/conf/db.properties.in @@ -41,6 +41,11 @@ db.cloud.maxWait=600000 db.cloud.minIdleConnections=5 db.cloud.connectionTimeout=30000 db.cloud.keepAliveTime=600000 +# HikariCP leak detection threshold in milliseconds. A value of 0 (or unset) disables leak detection. +# Useful for debugging borrowed DB connections that are not returned to the pool. HikariCP ignores values below 2000. +# db.cloud.leakDetectionThreshold=0 +# Enable HikariCP JMX MBeans for observing pool counters. Disabled by default. +# db.cloud.registerMbeans=false db.cloud.validationQuery=/* ping */ SELECT 1 db.cloud.testOnBorrow=true db.cloud.testWhileIdle=true diff --git a/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java b/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java index 18a90749e49c..ae1ca3e452d3 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java +++ b/framework/db/src/main/java/com/cloud/utils/db/TransactionLegacy.java @@ -38,6 +38,7 @@ import org.apache.commons.dbcp2.PoolableConnection; import org.apache.commons.dbcp2.PoolableConnectionFactory; import org.apache.commons.dbcp2.PoolingDataSource; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.pool2.ObjectPool; @@ -1065,6 +1066,8 @@ public static void initDataSource(Properties dbProps) { final Integer cloudMinIdleConnections = parseNumber(dbProps.getProperty("db.cloud.minIdleConnections"), Integer.class); final Long cloudConnectionTimeout = parseNumber(dbProps.getProperty("db.cloud.connectionTimeout"), Long.class); final Long cloudKeepAliveTimeout = parseNumber(dbProps.getProperty("db.cloud.keepAliveTime"), Long.class); + final Long cloudLeakDetectionThreshold = parseNumber(dbProps.getProperty("db.cloud.leakDetectionThreshold"), Long.class); + final Boolean cloudRegisterMbeans = BooleanUtils.toBooleanObject(dbProps.getProperty("db.cloud.registerMbeans")); final String cloudUsername = dbProps.getProperty("db.cloud.username"); final String cloudPassword = dbProps.getProperty("db.cloud.password"); final String cloudValidationQuery = dbProps.getProperty("db.cloud.validationQuery"); @@ -1107,7 +1110,7 @@ public static void initDataSource(Properties dbProps) { cloudUsername, cloudPassword, cloudMaxActive, cloudMaxIdle, cloudMaxWait, cloudTimeBtwEvictionRunsMillis, cloudMinEvcitableIdleTimeMillis, cloudTestWhileIdle, cloudTestOnBorrow, cloudValidationQuery, cloudMinIdleConnections, cloudConnectionTimeout, - cloudKeepAliveTimeout, isolationLevel, "cloud"); + cloudKeepAliveTimeout, cloudLeakDetectionThreshold, cloudRegisterMbeans, isolationLevel, "cloud"); // Configure the usage db final Integer usageMaxActive = parseNumber(dbProps.getProperty("db.usage.maxActive"), Integer.class); @@ -1116,6 +1119,8 @@ public static void initDataSource(Properties dbProps) { final Integer usageMinIdleConnections = parseNumber(dbProps.getProperty("db.usage.minIdleConnections"), Integer.class); final Long usageConnectionTimeout = parseNumber(dbProps.getProperty("db.usage.connectionTimeout"), Long.class); final Long usageKeepAliveTimeout = parseNumber(dbProps.getProperty("db.usage.keepAliveTime"), Long.class); + final Long usageLeakDetectionThreshold = parseNumber(dbProps.getProperty("db.usage.leakDetectionThreshold"), Long.class); + final Boolean usageRegisterMbeans = BooleanUtils.toBooleanObject(dbProps.getProperty("db.usage.registerMbeans")); final String usageUsername = dbProps.getProperty("db.usage.username"); final String usagePassword = dbProps.getProperty("db.usage.password"); @@ -1127,7 +1132,8 @@ public static void initDataSource(Properties dbProps) { s_usageDS = createDataSource(dbProps.getProperty("db.usage.connectionPoolLib"), usageUriAndDriver.first(), usageUsername, usagePassword, usageMaxActive, usageMaxIdle, usageMaxWait, null, null, null, null, null, - usageMinIdleConnections, usageConnectionTimeout, usageKeepAliveTimeout, isolationLevel, "usage"); + usageMinIdleConnections, usageConnectionTimeout, usageKeepAliveTimeout, usageLeakDetectionThreshold, + usageRegisterMbeans, isolationLevel, "usage"); try { // Configure the simulator db @@ -1137,6 +1143,8 @@ public static void initDataSource(Properties dbProps) { final Integer simulatorMinIdleConnections = parseNumber(dbProps.getProperty("db.simulator.minIdleConnections"), Integer.class); final Long simulatorConnectionTimeout = parseNumber(dbProps.getProperty("db.simulator.connectionTimeout"), Long.class); final Long simulatorKeepAliveTimeout = parseNumber(dbProps.getProperty("db.simulator.keepAliveTime"), Long.class); + final Long simulatorLeakDetectionThreshold = parseNumber(dbProps.getProperty("db.simulator.leakDetectionThreshold"), Long.class); + final Boolean simulatorRegisterMbeans = BooleanUtils.toBooleanObject(dbProps.getProperty("db.simulator.registerMbeans")); final String simulatorUsername = dbProps.getProperty("db.simulator.username"); final String simulatorPassword = dbProps.getProperty("db.simulator.password"); @@ -1167,7 +1175,8 @@ public static void initDataSource(Properties dbProps) { simulatorConnectionUri, simulatorUsername, simulatorPassword, simulatorMaxActive, simulatorMaxIdle, simulatorMaxWait, null, null, null, null, cloudValidationQuery, simulatorMinIdleConnections, simulatorConnectionTimeout, - simulatorKeepAliveTimeout, isolationLevel, "simulator"); + simulatorKeepAliveTimeout, simulatorLeakDetectionThreshold, simulatorRegisterMbeans, + isolationLevel, "simulator"); } catch (Exception e) { LOGGER.debug("Simulator DB properties are not available. Not initializing simulator DS"); } @@ -1269,7 +1278,8 @@ protected static String buildConnectionUri(String loadBalanceStrategy, String dr private static DataSource createDataSource(String connectionPoolLib, String uri, String username, String password, Integer maxActive, Integer maxIdle, Long maxWait, Long timeBtwnEvictionRuns, Long minEvictableIdleTime, Boolean testWhileIdle, Boolean testOnBorrow, String validationQuery, Integer minIdleConnections, - Long connectionTimeout, Long keepAliveTime, Integer isolationLevel, String dsName) { + Long connectionTimeout, Long keepAliveTime, Long leakDetectionThreshold, Boolean registerMbeans, + Integer isolationLevel, String dsName) { LOGGER.debug("Creating datasource for database: {} with connection pool lib: {}", dsName, connectionPoolLib); if (CONNECTION_POOL_LIB_DBCP.equals(connectionPoolLib)) { @@ -1277,12 +1287,13 @@ private static DataSource createDataSource(String connectionPoolLib, String uri, minEvictableIdleTime, testWhileIdle, testOnBorrow, validationQuery, isolationLevel); } return createHikaricpDataSource(uri, username, password, maxActive, maxIdle, maxWait, minIdleConnections, - connectionTimeout, keepAliveTime, isolationLevel, dsName); + connectionTimeout, keepAliveTime, leakDetectionThreshold, registerMbeans, isolationLevel, dsName); } private static DataSource createHikaricpDataSource(String uri, String username, String password, Integer maxActive, Integer maxIdle, Long maxWait, Integer minIdleConnections, Long connectionTimeout, Long keepAliveTime, + Long leakDetectionThreshold, Boolean registerMbeans, Integer isolationLevel, String dsName) { HikariConfig config = new HikariConfig(); config.setJdbcUrl(uri); @@ -1299,6 +1310,8 @@ private static DataSource createHikaricpDataSource(String uri, String username, config.setConnectionTimeout(ObjectUtils.defaultIfNull(connectionTimeout, 30000L)); config.setKeepaliveTime(ObjectUtils.defaultIfNull(keepAliveTime, 600000L)); + applyHikariDebugSettings(config, leakDetectionThreshold, registerMbeans, dsName); + String isolationLevelString = "TRANSACTION_READ_COMMITTED"; if (isolationLevel == Connection.TRANSACTION_SERIALIZABLE) { isolationLevelString = "TRANSACTION_SERIALIZABLE"; @@ -1326,6 +1339,21 @@ private static DataSource createHikaricpDataSource(String uri, String username, return dataSource; } + /** + * Applies optional HikariCP debugging aids (leak detection and JMX MBeans). Both are disabled by + * default; leakDetectionThreshold is only applied when set to a positive value (HikariCP treats 0 + * as disabled and ignores values below 2000ms). Package-private for unit testing. + */ + static void applyHikariDebugSettings(HikariConfig config, Long leakDetectionThreshold, Boolean registerMbeans, String dsName) { + if (leakDetectionThreshold != null && leakDetectionThreshold > 0) { + config.setLeakDetectionThreshold(leakDetectionThreshold); + } + config.setRegisterMbeans(ObjectUtils.defaultIfNull(registerMbeans, false)); + LOGGER.debug("HikariCP pool {}: leakDetectionThreshold={} ms, registerMbeans={}", dsName, + ObjectUtils.defaultIfNull(leakDetectionThreshold, 0L), + ObjectUtils.defaultIfNull(registerMbeans, false)); + } + private static DataSource createDbcpDataSource(String uri, String username, String password, Integer maxActive, Integer maxIdle, Long maxWait, Long timeBtwnEvictionRuns, Long minEvictableIdleTime, diff --git a/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java b/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java index 2e0af6fa1866..b5ca2bda6434 100644 --- a/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java +++ b/framework/db/src/test/java/com/cloud/utils/db/TransactionLegacyTest.java @@ -17,6 +17,7 @@ package com.cloud.utils.db; import com.cloud.utils.Pair; +import com.zaxxer.hikari.HikariConfig; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -114,4 +115,44 @@ public void buildConnectionUriTestUseSslTrue() { Assert.assertEquals("driver://host:5555/cloud?autoReconnect=false&useSSL=true", result); } + + @Test + public void applyHikariDebugSettingsDisabledByDefault() { + HikariConfig config = new HikariConfig(); + + TransactionLegacy.applyHikariDebugSettings(config, null, null, "cloud"); + + Assert.assertEquals(0L, config.getLeakDetectionThreshold()); + Assert.assertFalse(config.isRegisterMbeans()); + } + + @Test + public void applyHikariDebugSettingsZeroThresholdKeepsLeakDetectionDisabled() { + HikariConfig config = new HikariConfig(); + + TransactionLegacy.applyHikariDebugSettings(config, 0L, false, "cloud"); + + Assert.assertEquals(0L, config.getLeakDetectionThreshold()); + Assert.assertFalse(config.isRegisterMbeans()); + } + + @Test + public void applyHikariDebugSettingsEnablesLeakDetection() { + HikariConfig config = new HikariConfig(); + + TransactionLegacy.applyHikariDebugSettings(config, 60000L, null, "cloud"); + + Assert.assertEquals(60000L, config.getLeakDetectionThreshold()); + Assert.assertFalse(config.isRegisterMbeans()); + } + + @Test + public void applyHikariDebugSettingsEnablesRegisterMbeans() { + HikariConfig config = new HikariConfig(); + + TransactionLegacy.applyHikariDebugSettings(config, null, true, "cloud"); + + Assert.assertEquals(0L, config.getLeakDetectionThreshold()); + Assert.assertTrue(config.isRegisterMbeans()); + } }