package com.zaxxer.hikari.pool; import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_AUTOCOMMIT; import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_CATALOG; import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_ISOLATION; import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_NETTIMEOUT; import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_READONLY; import static com.zaxxer.hikari.util.UtilityElf.createInstance; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import java.lang.management.ManagementFactory; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicReference; import javax.management.MBeanServer; import javax.management.ObjectName; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.metrics.MetricsTracker; import com.zaxxer.hikari.util.ClockSource; import com.zaxxer.hikari.util.DriverDataSource; import com.zaxxer.hikari.util.PropertyElf; import com.zaxxer.hikari.util.UtilityElf; import com.zaxxer.hikari.util.UtilityElf.DefaultThreadFactory; abstract class PoolBase { private final Logger LOGGER = LoggerFactory.getLogger(PoolBase.class); protected final HikariConfig config; protected final String poolName; protected long connectionTimeout; protected long validationTimeout; private static final String[] RESET_STATES = {"readOnly", "autoCommit", "isolation", "catalog", "netTimeout"}; private static final int UNINITIALIZED = -1; private static final int TRUE = 1; private static final int FALSE = 0; private int networkTimeout; private int isNetworkTimeoutSupported; private int isQueryTimeoutSupported; private int defaultTransactionIsolation; private int transactionIsolation; private Executor netTimeoutExecutor; private DataSource dataSource; private final String catalog; private final boolean isReadOnly; private final boolean isAutoCommit; private final boolean isUseJdbc4Validation; private final boolean isIsolateInternalQueries; private final AtomicReference<Throwable> lastConnectionFailure; private volatile boolean isValidChecked; PoolBase(final HikariConfig config) { this.config = config; this.networkTimeout = UNINITIALIZED; this.catalog = config.getCatalog(); this.isReadOnly = config.isReadOnly(); this.isAutoCommit = config.isAutoCommit(); this.transactionIsolation = UtilityElf.getTransactionIsolation(config.getTransactionIsolation()); this.isQueryTimeoutSupported = UNINITIALIZED; this.isNetworkTimeoutSupported = UNINITIALIZED; this.isUseJdbc4Validation = config.getConnectionTestQuery() == null; this.isIsolateInternalQueries = config.isIsolateInternalQueries(); this.poolName = config.getPoolName(); this.connectionTimeout = config.getConnectionTimeout(); this.validationTimeout = config.getValidationTimeout(); this.lastConnectionFailure = new AtomicReference<>(); initializeDataSource(); } /** {@inheritDoc} */ @Override public String toString() { return poolName; } abstract void releaseConnection(final PoolEntry poolEntry); // *********************************************************************** // JDBC methods // *********************************************************************** void quietlyCloseConnection(final Connection connection, final String closureReason) { if (connection != null) { try { LOGGER.debug("{} - Closing connection {}: {}", poolName, connection, closureReason); try { setNetworkTimeout(connection, SECONDS.toMillis(15)); } finally { connection.close(); // continue with the close even if setNetworkTimeout() throws } } catch (Throwable e) { LOGGER.debug("{} - Closing connection {} failed", poolName, connection, e); } } } boolean isConnectionAlive(final Connection connection) { try { if (isUseJdbc4Validation) { return connection.isValid((int) MILLISECONDS.toSeconds(Math.max(1000L, validationTimeout))); } setNetworkTimeout(connection, validationTimeout); try (Statement statement = connection.createStatement()) { if (isNetworkTimeoutSupported != TRUE) { setQueryTimeout(statement, (int) MILLISECONDS.toSeconds(Math.max(1000L, validationTimeout))); } statement.execute(config.getConnectionTestQuery()); } if (isIsolateInternalQueries && !isReadOnly && !isAutoCommit) { connection.rollback(); } setNetworkTimeout(connection, networkTimeout); return true; } catch (SQLException e) { lastConnectionFailure.set(e); LOGGER.warn("{} - Failed to validate connection {} ({})", poolName, connection, e.getMessage()); return false; } } Throwable getLastConnectionFailure() { return lastConnectionFailure.getAndSet(null); } public DataSource getUnwrappedDataSource() { return dataSource; } // *********************************************************************** // PoolEntry methods // *********************************************************************** PoolEntry newPoolEntry() throws Exception { return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit); } void resetConnectionState(final Connection connection, final ProxyConnection proxyConnection, final int dirtyBits) throws SQLException { int resetBits = 0; if ((dirtyBits & DIRTY_BIT_READONLY) != 0 && proxyConnection.getReadOnlyState() != isReadOnly) { connection.setReadOnly(isReadOnly); resetBits |= DIRTY_BIT_READONLY; } if ((dirtyBits & DIRTY_BIT_AUTOCOMMIT) != 0 && proxyConnection.getAutoCommitState() != isAutoCommit) { connection.setAutoCommit(isAutoCommit); resetBits |= DIRTY_BIT_AUTOCOMMIT; } if ((dirtyBits & DIRTY_BIT_ISOLATION) != 0 && proxyConnection.getTransactionIsolationState() != transactionIsolation) { connection.setTransactionIsolation(transactionIsolation); resetBits |= DIRTY_BIT_ISOLATION; } if ((dirtyBits & DIRTY_BIT_CATALOG) != 0 && catalog != null && !catalog.equals(proxyConnection.getCatalogState())) { connection.setCatalog(catalog); resetBits |= DIRTY_BIT_CATALOG; } if ((dirtyBits & DIRTY_BIT_NETTIMEOUT) != 0 && proxyConnection.getNetworkTimeoutState() != networkTimeout) { setNetworkTimeout(connection, networkTimeout); resetBits |= DIRTY_BIT_NETTIMEOUT; } if (resetBits != 0 && LOGGER.isDebugEnabled()) { LOGGER.debug("{} - Reset ({}) on connection {}", poolName, stringFromResetBits(resetBits), connection); } } void shutdownNetworkTimeoutExecutor() { if (netTimeoutExecutor instanceof ThreadPoolExecutor) { ((ThreadPoolExecutor) netTimeoutExecutor).shutdownNow(); } } // *********************************************************************** // JMX methods // *********************************************************************** /** * Register MBeans for HikariConfig and HikariPool. * * @param pool a HikariPool instance */ void registerMBeans(final HikariPool hikariPool) { if (!config.isRegisterMbeans()) { return; } try { final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); final ObjectName beanConfigName = new ObjectName("com.zaxxer.hikari:type=PoolConfig (" + poolName + ")"); final ObjectName beanPoolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")"); if (!mBeanServer.isRegistered(beanConfigName)) { mBeanServer.registerMBean(config, beanConfigName); mBeanServer.registerMBean(hikariPool, beanPoolName); } else { LOGGER.error("{} - You cannot use the same pool name for separate pool instances.", poolName); } } catch (Exception e) { LOGGER.warn("{} - Failed to register management beans.", poolName, e); } } /** * Unregister MBeans for HikariConfig and HikariPool. */ void unregisterMBeans() { if (!config.isRegisterMbeans()) { return; } try { final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); final ObjectName beanConfigName = new ObjectName("com.zaxxer.hikari:type=PoolConfig (" + poolName + ")"); final ObjectName beanPoolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")"); if (mBeanServer.isRegistered(beanConfigName)) { mBeanServer.unregisterMBean(beanConfigName); mBeanServer.unregisterMBean(beanPoolName); } } catch (Exception e) { LOGGER.warn("{} - Failed to unregister management beans.", poolName, e); } } // *********************************************************************** // Private methods // *********************************************************************** /** * Create/initialize the underlying DataSource. * * @return a DataSource instance */ private void initializeDataSource() { final String jdbcUrl = config.getJdbcUrl(); final String username = config.getUsername(); final String password = config.getPassword(); final String dsClassName = config.getDataSourceClassName(); final String driverClassName = config.getDriverClassName(); final Properties dataSourceProperties = config.getDataSourceProperties(); DataSource dataSource = config.getDataSource(); if (dsClassName != null && dataSource == null) { dataSource = createInstance(dsClassName, DataSource.class); PropertyElf.setTargetFromProperties(dataSource, dataSourceProperties); } else if (jdbcUrl != null && dataSource == null) { dataSource = new DriverDataSource(jdbcUrl, driverClassName, dataSourceProperties, username, password); } if (dataSource != null) { setLoginTimeout(dataSource, connectionTimeout); createNetworkTimeoutExecutor(dataSource, dsClassName, jdbcUrl); } this.dataSource = dataSource; } Connection newConnection() throws Exception { Connection connection = null; try { String username = config.getUsername(); String password = config.getPassword(); connection = (username == null) ? dataSource.getConnection() : dataSource.getConnection(username, password); setupConnection(connection); lastConnectionFailure.set(null); return connection; } catch (Exception e) { lastConnectionFailure.set(e); quietlyCloseConnection(connection, "(Failed to create/set connection)"); throw e; } } /** * Setup a connection initial state. * * @param connection a Connection * @throws SQLException thrown from driver */ private void setupConnection(final Connection connection) throws SQLException { if (networkTimeout == UNINITIALIZED) { networkTimeout = getAndSetNetworkTimeout(connection, validationTimeout); } else { setNetworkTimeout(connection, validationTimeout); } checkDriverSupport(connection); connection.setReadOnly(isReadOnly); connection.setAutoCommit(isAutoCommit); if (transactionIsolation != defaultTransactionIsolation) { connection.setTransactionIsolation(transactionIsolation); } if (catalog != null) { connection.setCatalog(catalog); } executeSql(connection, config.getConnectionInitSql(), true); setNetworkTimeout(connection, networkTimeout); } /** * Execute isValid() or connection test query. * * @param connection a Connection to check */ private void checkDriverSupport(final Connection connection) throws SQLException { if (!isValidChecked) { if (isUseJdbc4Validation) { try { connection.isValid(1); } catch (Throwable e) { LOGGER.error("{} - Failed to execute isValid() for connection, configure connection test query. ({})", poolName, e.getMessage()); throw e; } } else { try { executeSql(connection, config.getConnectionTestQuery(), false); } catch (Throwable e) { LOGGER.error("{} - Failed to execute connection test query. ({})", poolName, e.getMessage()); throw e; } } defaultTransactionIsolation = connection.getTransactionIsolation(); if (transactionIsolation == -1) { transactionIsolation = defaultTransactionIsolation; } isValidChecked = true; } } /** * Set the query timeout, if it is supported by the driver. * * @param statement a statement to set the query timeout on * @param timeoutSec the number of seconds before timeout */ private void setQueryTimeout(final Statement statement, final int timeoutSec) { if (isQueryTimeoutSupported != FALSE) { try { statement.setQueryTimeout(timeoutSec); isQueryTimeoutSupported = TRUE; } catch (Throwable e) { if (isQueryTimeoutSupported == UNINITIALIZED) { isQueryTimeoutSupported = FALSE; LOGGER.warn("{} - Failed to set query timeout for statement. ({})", poolName, e.getMessage()); } } } } /** * Set the network timeout, if <code>isUseNetworkTimeout</code> is <code>true</code> and the * driver supports it. Return the pre-existing value of the network timeout. * * @param connection the connection to set the network timeout on * @param timeoutMs the number of milliseconds before timeout * @return the pre-existing network timeout value */ private int getAndSetNetworkTimeout(final Connection connection, final long timeoutMs) { if (isNetworkTimeoutSupported != FALSE) { try { final int originalTimeout = connection.getNetworkTimeout(); connection.setNetworkTimeout(netTimeoutExecutor, (int) timeoutMs); isNetworkTimeoutSupported = TRUE; return originalTimeout; } catch (Throwable e) { if (isNetworkTimeoutSupported == UNINITIALIZED) { isNetworkTimeoutSupported = FALSE; LOGGER.warn("{} - Failed to get/set network timeout for connection. ({})", poolName, e.getMessage()); if (validationTimeout < SECONDS.toMillis(1)) { LOGGER.warn("{} - A validationTimeout of less than 1 second cannot be honored on drivers without setNetworkTimeout() support.", poolName); } else if (validationTimeout % SECONDS.toMillis(1) != 0) { LOGGER.warn("{} - A validationTimeout with fractional second granularity cannot be honored on drivers without setNetworkTimeout() support.", poolName); } } } } return 0; } /** * Set the network timeout, if <code>isUseNetworkTimeout</code> is <code>true</code> and the * driver supports it. * * @param connection the connection to set the network timeout on * @param timeoutMs the number of milliseconds before timeout * @throws SQLException throw if the connection.setNetworkTimeout() call throws */ private void setNetworkTimeout(final Connection connection, final long timeoutMs) throws SQLException { if (isNetworkTimeoutSupported == TRUE) { connection.setNetworkTimeout(netTimeoutExecutor, (int) timeoutMs); } } /** * Execute the user-specified init SQL. * * @param connection the connection to initialize * @param sql the SQL to execute * @param isCommit whether to commit the SQL after execution or not * @throws SQLException throws if the init SQL execution fails */ private void executeSql(final Connection connection, final String sql, final boolean isCommit) throws SQLException { if (sql != null) { try (Statement statement = connection.createStatement()) { // connection was created a few milliseconds before, so set query timeout is omitted (we assume it will succeed) statement.execute(sql); } if (isIsolateInternalQueries && !isReadOnly && !isAutoCommit) { if (isCommit) { connection.commit(); } else { connection.rollback(); } } } } private void createNetworkTimeoutExecutor(final DataSource dataSource, final String dsClassName, final String jdbcUrl) { // Temporary hack for MySQL issue: http://bugs.mysql.com/bug.php?id=75615 if ((dsClassName != null && dsClassName.contains("Mysql")) || (jdbcUrl != null && jdbcUrl.contains("mysql")) || (dataSource != null && dataSource.getClass().getName().contains("Mysql"))) { netTimeoutExecutor = new SynchronousExecutor(); } else { ThreadFactory threadFactory = config.getThreadFactory(); threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory(poolName + " network timeout executor", true); ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(threadFactory); executor.setKeepAliveTime(15, SECONDS); executor.allowCoreThreadTimeOut(true); netTimeoutExecutor = executor; } } /** * Set the loginTimeout on the specified DataSource. * * @param dataSource the DataSource * @param connectionTimeout the timeout in milliseconds */ private void setLoginTimeout(final DataSource dataSource, final long connectionTimeout) { if (connectionTimeout != Integer.MAX_VALUE) { try { dataSource.setLoginTimeout((int) MILLISECONDS.toSeconds(Math.max(1000L, connectionTimeout))); } catch (Throwable e) { LOGGER.warn("{} - Failed to set login timeout for data source. ({})", poolName, e.getMessage()); } } } /** * This will create a string for debug logging. Given a set of "reset bits", this * method will return a concatenated string, for example: * * Input : 0b00110 * Output: "autoCommit, isolation" * * @param bits a set of "reset bits" * @return a string of which states were reset */ private String stringFromResetBits(final int bits) { final StringBuilder sb = new StringBuilder(); for (int ndx = 0; ndx < RESET_STATES.length; ndx++) { if ( (bits & (0b1 << ndx)) != 0) { sb.append(RESET_STATES[ndx]).append(", "); } } sb.setLength(sb.length() - 2); // trim trailing comma return sb.toString(); } // *********************************************************************** // Private Static Classes // *********************************************************************** /** * Special executor used only to work around a MySQL issue that has not been addressed. * MySQL issue: http://bugs.mysql.com/bug.php?id=75615 */ private static class SynchronousExecutor implements Executor { /** {@inheritDoc} */ @Override public void execute(Runnable command) { try { command.run(); } catch (Throwable t) { LoggerFactory.getLogger(PoolBase.class).debug("Failed to execute: {}", command, t); } } } /** * A class that delegates to a MetricsTracker implementation. The use of a delegate * allows us to use the NopMetricsTrackerDelegate when metrics are disabled, which in * turn allows the JIT to completely optimize away to callsites to record metrics. */ static class MetricsTrackerDelegate implements AutoCloseable { final MetricsTracker tracker; protected MetricsTrackerDelegate() { this.tracker = null; } MetricsTrackerDelegate(MetricsTracker tracker) { this.tracker = tracker; } @Override public void close() { tracker.close(); } void recordConnectionUsage(final PoolEntry poolEntry) { tracker.recordConnectionUsageMillis(poolEntry.getMillisSinceBorrowed()); } /** * @param poolEntry * @param now */ void recordBorrowStats(final PoolEntry poolEntry, final long startTime) { final long now = ClockSource.INSTANCE.currentTime(); poolEntry.lastBorrowed = now; tracker.recordConnectionAcquiredNanos(ClockSource.INSTANCE.elapsedNanos(startTime, now)); } void recordConnectionTimeout() { tracker.recordConnectionTimeout(); } } /** * A no-op implementation of the MetricsTrackerDelegate that is used when metrics capture is * disabled. */ static final class NopMetricsTrackerDelegate extends MetricsTrackerDelegate { @Override void recordConnectionUsage(final PoolEntry poolEntry) { // no-op } @Override public void close() { // no-op } @Override void recordBorrowStats(final PoolEntry poolEntry, final long startTime) { // no-op } @Override void recordConnectionTimeout() { // no-op } } }