diff --git a/src/main/java/com/zaxxer/hikari/HikariConfig.java b/src/main/java/com/zaxxer/hikari/HikariConfig.java index 8e46953b..db9bd2d8 100644 --- a/src/main/java/com/zaxxer/hikari/HikariConfig.java +++ b/src/main/java/com/zaxxer/hikari/HikariConfig.java @@ -80,6 +80,7 @@ public class HikariConfig implements HikariConfigMXBean private String dataSourceClassName; private String dataSourceJndiName; private String driverClassName; + private String exceptionOverrideClassName; private String jdbcUrl; private String poolName; private String schema; @@ -812,7 +813,8 @@ public class HikariConfig implements HikariConfigMXBean * * @return the default schema name */ - public String getSchema() { + public String getSchema() + { return schema; } @@ -827,6 +829,50 @@ public class HikariConfig implements HikariConfigMXBean this.schema = schema; } + /** + * Get the user supplied SQLExceptionOverride class name. + * + * @return the user supplied SQLExceptionOverride class name + * @see SQLExceptionOverride + */ + public String getExceptionOverrideClassName() + { + return this.exceptionOverrideClassName; + } + + /** + * Set the user supplied SQLExceptionOverride class name. + * + * @param exceptionOverrideClassName the user supplied SQLExceptionOverride class name + * @see SQLExceptionOverride + */ + public void setExceptionOverrideClassName(String exceptionOverrideClassName) + { + checkIfSealed(); + + Class overrideClass = attemptFromContextLoader(exceptionOverrideClassName); + try { + if (overrideClass == null) { + overrideClass = this.getClass().getClassLoader().loadClass(exceptionOverrideClassName); + LOGGER.debug("SQLExceptionOverride class {} found in the HikariConfig class classloader {}", exceptionOverrideClassName, this.getClass().getClassLoader()); + } + } catch (ClassNotFoundException e) { + LOGGER.error("Failed to load SQLExceptionOverride class {} from HikariConfig class classloader {}", exceptionOverrideClassName, this.getClass().getClassLoader()); + } + + if (overrideClass == null) { + throw new RuntimeException("Failed to load SQLExceptionOverride class " + exceptionOverrideClassName + " in either of HikariConfig class loader or Thread context classloader"); + } + + try { + overrideClass.getConstructor().newInstance(); + this.exceptionOverrideClassName = exceptionOverrideClassName; + } + catch (Exception e) { + throw new RuntimeException("Failed to instantiate class " + exceptionOverrideClassName, e); + } + } + /** * Set the default transaction isolation level. The specified value is the * constant name from the Connection class, eg. diff --git a/src/main/java/com/zaxxer/hikari/SQLExceptionOverride.java b/src/main/java/com/zaxxer/hikari/SQLExceptionOverride.java new file mode 100644 index 00000000..17168d8e --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/SQLExceptionOverride.java @@ -0,0 +1,30 @@ +package com.zaxxer.hikari; + +import java.sql.SQLException; + +/** + * Users can implement this interface to override the default SQLException handling + * of HikariCP. By the time an instance of this interface is invoked HikariCP has + * already made a determination to evict the Connection from the pool. + * + * If the {@link #adjudicate(SQLException)} method returns {@link Override#CONTINUE_EVICT} the eviction will occur, but if the + * method returns {@link Override#DO_NOT_EVICT} the eviction will be elided. + */ +public interface SQLExceptionOverride { + enum Override { + CONTINUE_EVICT, + DO_NOT_EVICT + } + + /** + * If this method returns {@link Override#CONTINUE_EVICT} then Connection eviction will occur, but if it + * returns {@link Override#DO_NOT_EVICT} the eviction will be elided. + * + * @param sqlException the #SQLException to adjudicate + * @return either one of {@link Override#CONTINUE_EVICT} or {@link Override#DO_NOT_EVICT} + */ + default Override adjudicate(final SQLException sqlException) + { + return Override.CONTINUE_EVICT; + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/PoolBase.java b/src/main/java/com/zaxxer/hikari/pool/PoolBase.java index 631d8a2a..991ab024 100644 --- a/src/main/java/com/zaxxer/hikari/pool/PoolBase.java +++ b/src/main/java/com/zaxxer/hikari/pool/PoolBase.java @@ -17,6 +17,7 @@ package com.zaxxer.hikari.pool; import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.SQLExceptionOverride; import com.zaxxer.hikari.metrics.IMetricsTracker; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; import com.zaxxer.hikari.util.DriverDataSource; @@ -64,6 +65,8 @@ abstract class PoolBase long connectionTimeout; long validationTimeout; + SQLExceptionOverride exceptionOverride; + private static final String[] RESET_STATES = {"readOnly", "autoCommit", "isolation", "catalog", "netTimeout", "schema"}; private static final int UNINITIALIZED = -1; private static final int TRUE = 1; @@ -95,6 +98,7 @@ abstract class PoolBase this.schema = config.getSchema(); this.isReadOnly = config.isReadOnly(); this.isAutoCommit = config.isAutoCommit(); + this.exceptionOverride = UtilityElf.createInstance(config.getExceptionOverrideClassName(), SQLExceptionOverride.class); this.transactionIsolation = UtilityElf.getTransactionIsolation(config.getTransactionIsolation()); this.isQueryTimeoutSupported = UNINITIALIZED; diff --git a/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java b/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java index 2b452567..07c670b8 100644 --- a/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java +++ b/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java @@ -128,6 +128,11 @@ final class PoolEntry implements IConcurrentBagEntry return elapsedMillis(lastBorrowed); } + PoolBase getPoolBase() + { + return hikariPool; + } + /** {@inheritDoc} */ @Override public String toString() diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java b/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java index db8b12bf..653ad31c 100644 --- a/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java @@ -16,6 +16,7 @@ package com.zaxxer.hikari.pool; +import com.zaxxer.hikari.SQLExceptionOverride; import com.zaxxer.hikari.util.FastList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +28,7 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.Executor; +import static com.zaxxer.hikari.SQLExceptionOverride.Override.DO_NOT_EVICT; import static com.zaxxer.hikari.util.ClockSource.currentTime; /** @@ -83,7 +85,13 @@ public abstract class ProxyConnection implements Connection ERROR_CODES.add(2399); } - protected ProxyConnection(final PoolEntry poolEntry, final Connection connection, final FastList openStatements, final ProxyLeakTask leakTask, final long now, final boolean isReadOnly, final boolean isAutoCommit) { + protected ProxyConnection(final PoolEntry poolEntry, + final Connection connection, + final FastList openStatements, + final ProxyLeakTask leakTask, + final long now, + final boolean isReadOnly, + final boolean isAutoCommit) { this.poolEntry = poolEntry; this.delegate = connection; this.openStatements = openStatements; @@ -143,9 +151,12 @@ public abstract class ProxyConnection implements Connection return poolEntry; } + @SuppressWarnings("ConstantConditions") final SQLException checkException(SQLException sqle) { + boolean evict = false; SQLException nse = sqle; + final SQLExceptionOverride exceptionOverride = poolEntry.getPoolBase().exceptionOverride; for (int depth = 0; delegate != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++) { final String sqlState = nse.getSQLState(); if (sqlState != null && sqlState.startsWith("08") @@ -153,18 +164,28 @@ public abstract class ProxyConnection implements Connection || ERROR_STATES.contains(sqlState) || ERROR_CODES.contains(nse.getErrorCode())) { + if (exceptionOverride != null && exceptionOverride.adjudicate(nse) == DO_NOT_EVICT) { + break; + } + // broken connection - LOGGER.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})", - poolEntry.getPoolName(), delegate, sqlState, nse.getErrorCode(), nse); - leakTask.cancel(); - poolEntry.evict("(connection is broken)"); - delegate = ClosedConnection.CLOSED_CONNECTION; + evict = true; + break; } else { nse = nse.getNextException(); } } + if (evict) { + SQLException exception = (nse != null) ? nse : sqle; + LOGGER.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})", + poolEntry.getPoolName(), delegate, exception.getSQLState(), exception.getErrorCode(), exception); + leakTask.cancel(); + poolEntry.evict("(connection is broken)"); + delegate = ClosedConnection.CLOSED_CONNECTION; + } + return sqle; } diff --git a/src/test/java/com/zaxxer/hikari/pool/TestConnections.java b/src/test/java/com/zaxxer/hikari/pool/TestConnections.java index 92b80b2e..0275b132 100644 --- a/src/test/java/com/zaxxer/hikari/pool/TestConnections.java +++ b/src/test/java/com/zaxxer/hikari/pool/TestConnections.java @@ -19,6 +19,7 @@ package com.zaxxer.hikari.pool; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariPoolMXBean; +import com.zaxxer.hikari.SQLExceptionOverride; import com.zaxxer.hikari.mocks.StubConnection; import com.zaxxer.hikari.mocks.StubDataSource; import com.zaxxer.hikari.mocks.StubStatement; @@ -40,6 +41,7 @@ import static org.junit.Assert.*; /** * @author Brett Wooldridge */ +@SuppressWarnings({"SqlDialectInspection", "SqlNoDataSourceInspection"}) public class TestConnections { @Before @@ -258,6 +260,81 @@ public class TestConnections } } + @Test + public void testEviction2() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setExceptionOverrideClassName(OverrideHandler.class.getName()); + + try (HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + + while (pool.getTotalConnections() < 5) { + quietlySleep(100L); + } + + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + + try { + statement.getMaxFieldSize(); + } catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + } + + assertEquals("Total connections not as expected", 5, pool.getTotalConnections()); + assertEquals("Idle connections not as expected", 5, pool.getIdleConnections()); + } + } + + @Test + public void testEviction3() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + + while (pool.getTotalConnections() < 5) { + quietlySleep(100L); + } + + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + + try { + statement.getMaxFieldSize(); + } catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + } + + assertEquals("Total connections not as expected", 4, pool.getTotalConnections()); + assertEquals("Idle connections not as expected", 4, pool.getIdleConnections()); + } + } + @Test public void testEvictAllRefill() throws Exception { HikariConfig config = newHikariConfig(); @@ -735,12 +812,13 @@ public class TestConnections } } - class StubDataSourceWithErrorSwitch extends StubDataSource { + static class StubDataSourceWithErrorSwitch extends StubDataSource + { private boolean errorOnConnection = false; /** {@inheritDoc} */ @Override - public Connection getConnection() throws SQLException { + public Connection getConnection() { if (!errorOnConnection) { return new StubConnection(); } @@ -753,4 +831,11 @@ public class TestConnections } } + public static class OverrideHandler implements SQLExceptionOverride + { + @java.lang.Override + public Override adjudicate(SQLException sqlException) { + return (sqlException.getSQLState().equals("08999")) ? Override.DO_NOT_EVICT : Override.CONTINUE_EVICT; + } + } }