diff --git a/src/main/java/com/zaxxer/hikari/pool/HikariPool.java b/src/main/java/com/zaxxer/hikari/pool/HikariPool.java index 2677103e..1897d7ec 100644 --- a/src/main/java/com/zaxxer/hikari/pool/HikariPool.java +++ b/src/main/java/com/zaxxer/hikari/pool/HikariPool.java @@ -1,652 +1,652 @@ -/* - * Copyright (C) 2013,2014 Brett Wooldridge - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.zaxxer.hikari.pool; - -import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.STATE_IN_USE; -import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.STATE_NOT_IN_USE; -import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.STATE_REMOVED; -import static com.zaxxer.hikari.util.UtilityElf.createThreadPoolExecutor; -import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; - -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.SQLTimeoutException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import javax.sql.DataSource; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.health.HealthCheckRegistry; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariPoolMXBean; -import com.zaxxer.hikari.metrics.CodahaleHealthChecker; -import com.zaxxer.hikari.metrics.CodahaleMetricsTrackerFactory; -import com.zaxxer.hikari.metrics.MetricsTracker; -import com.zaxxer.hikari.metrics.MetricsTracker.MetricsContext; -import com.zaxxer.hikari.metrics.MetricsTrackerFactory; -import com.zaxxer.hikari.metrics.PoolStats; -import com.zaxxer.hikari.proxy.ConnectionProxy; -import com.zaxxer.hikari.proxy.IHikariConnectionProxy; -import com.zaxxer.hikari.proxy.ProxyFactory; -import com.zaxxer.hikari.util.ClockSource; -import com.zaxxer.hikari.util.ConcurrentBag; -import com.zaxxer.hikari.util.ConcurrentBag.IBagStateListener; -import com.zaxxer.hikari.util.DefaultThreadFactory; -import com.zaxxer.hikari.util.PropertyElf; - -/** - * This is the primary connection pool class that provides the basic - * pooling behavior for HikariCP. - * - * @author Brett Wooldridge - */ -public class HikariPool implements HikariPoolMXBean, IBagStateListener -{ - final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - private static final ClockSource clockSource = ClockSource.INSTANCE; - - private final long ALIVE_BYPASS_WINDOW_MS = Long.getLong("com.zaxxer.hikari.aliveBypassWindow", TimeUnit.SECONDS.toMillis(1)); - private final long HOUSEKEEPING_PERIOD_MS = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", TimeUnit.SECONDS.toMillis(30)); - - private static final int POOL_NORMAL = 0; - private static final int POOL_SUSPENDED = 1; - private static final int POOL_SHUTDOWN = 2; - - final PoolElf poolElf; - final HikariConfig config; - final ConcurrentBag connectionBag; - final ScheduledThreadPoolExecutor houseKeepingExecutorService; - - private final AtomicInteger totalConnections; - private final ThreadPoolExecutor addConnectionExecutor; - private final ThreadPoolExecutor closeConnectionExecutor; - - private volatile int poolState; - private long connectionTimeout; - - private final String poolName; - private final LeakTask leakTask; - private final DataSource dataSource; - private final SuspendResumeLock suspendResumeLock; - private final AtomicReference lastConnectionFailure; - - private volatile MetricsTracker metricsTracker; - private boolean isRecordMetrics; - - /** - * Construct a HikariPool with the specified configuration. - * - * @param config a HikariConfig instance - */ - public HikariPool(final HikariConfig config) - { - this.config = config; - - this.poolElf = new PoolElf(config); - this.dataSource = poolElf.initializeDataSource(); - - this.poolName = config.getPoolName(); - this.connectionBag = new ConcurrentBag<>(this); - this.totalConnections = new AtomicInteger(); - this.connectionTimeout = config.getConnectionTimeout(); - this.lastConnectionFailure = new AtomicReference<>(); - this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock(true) : SuspendResumeLock.FAUX_LOCK; - - this.addConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), "Hikari connection filler (pool " + poolName + ")", config.getThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); - this.closeConnectionExecutor = createThreadPoolExecutor(4, "Hikari connection closer (pool " + poolName + ")", config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); - - if (config.getScheduledExecutorService() == null) { - ThreadFactory threadFactory = config.getThreadFactory() != null ? config.getThreadFactory() : new DefaultThreadFactory("Hikari housekeeper (pool " + poolName + ")", true); - this.houseKeepingExecutorService = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy()); - this.houseKeepingExecutorService.scheduleAtFixedRate(new HouseKeeper(), HOUSEKEEPING_PERIOD_MS, HOUSEKEEPING_PERIOD_MS, TimeUnit.MILLISECONDS); - this.houseKeepingExecutorService.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); - this.houseKeepingExecutorService.setRemoveOnCancelPolicy(true); - } - else { - this.houseKeepingExecutorService = config.getScheduledExecutorService(); - } - - this.leakTask = new LeakTask(config.getLeakDetectionThreshold(), houseKeepingExecutorService); - - if (config.getMetricsTrackerFactory() != null) { - setMetricsTrackerFactory(config.getMetricsTrackerFactory()); - } - else { - setMetricRegistry(config.getMetricRegistry()); - } - - setHealthCheckRegistry(config.getHealthCheckRegistry()); - - poolElf.registerMBeans(this); - - PropertyElf.flushCaches(); - - initializeConnections(); - } - - /** - * Get a connection from the pool, or timeout after connectionTimeout milliseconds. - * - * @return a java.sql.Connection instance - * @throws SQLException thrown if a timeout occurs trying to obtain a connection - */ - public final Connection getConnection() throws SQLException - { - return getConnection(connectionTimeout); - } - - /** - * Get a connection from the pool, or timeout after the specified number of milliseconds. - * - * @param hardTimeout the maximum time to wait for a connection from the pool - * @return a java.sql.Connection instance - * @throws SQLException thrown if a timeout occurs trying to obtain a connection - */ - public final Connection getConnection(final long hardTimeout) throws SQLException - { - suspendResumeLock.acquire(); - final long startTime = clockSource.currentTime(); - - try { - long timeout = hardTimeout; - final MetricsContext metricsContext = (isRecordMetrics ? metricsTracker.recordConnectionRequest() : MetricsTracker.NO_CONTEXT); - do { - final PoolBagEntry bagEntry = connectionBag.borrow(timeout, TimeUnit.MILLISECONDS); - if (bagEntry == null) { - break; // We timed out... break and throw exception - } - - final long now = clockSource.currentTime(); - if (bagEntry.evicted || (clockSource.elapsedMillis(bagEntry.lastAccess, now) > ALIVE_BYPASS_WINDOW_MS && !poolElf.isConnectionAlive(bagEntry.connection, lastConnectionFailure))) { - closeConnection(bagEntry, "(connection evicted or dead)"); // Throw away the dead connection and try again - timeout = hardTimeout - clockSource.elapsedMillis(startTime, now); - } - else { - metricsContext.setConnectionLastOpen(bagEntry, now); - metricsContext.stop(); - return ProxyFactory.getProxyConnection(bagEntry, leakTask.start(bagEntry), now); - } - } - while (timeout > 0L); - } - catch (InterruptedException e) { - throw new SQLException("Interrupted during connection acquisition", e); - } - finally { - suspendResumeLock.release(); - } - - logPoolState("Timeout failure "); - String sqlState = null; - final Throwable originalException = lastConnectionFailure.getAndSet(null); - if (originalException instanceof SQLException) { - sqlState = ((SQLException) originalException).getSQLState(); - } - throw new SQLTimeoutException(String.format("Timeout after %dms of waiting for a connection.", clockSource.elapsedMillis(startTime)), sqlState, originalException); - } - - /** - * Release a connection back to the pool, or permanently close it if it is broken. - * - * @param bagEntry the PoolBagEntry to release back to the pool - */ - public final void releaseConnection(final PoolBagEntry bagEntry) - { - metricsTracker.recordConnectionUsage(bagEntry); - - if (bagEntry.evicted) { - closeConnection(bagEntry, "(connection broken or evicted)"); - } - else { - connectionBag.requite(bagEntry); - } - } - - /** - * Shutdown the pool, closing all idle connections and aborting or closing - * active connections. - * - * @throws InterruptedException thrown if the thread is interrupted during shutdown - */ - public final synchronized void shutdown() throws InterruptedException - { - try { - poolState = POOL_SHUTDOWN; - - LOGGER.info("Hikari pool {} is shutting down.", poolName); - logPoolState("Before shutdown "); - - connectionBag.close(); - softEvictConnections(); - addConnectionExecutor.shutdownNow(); - addConnectionExecutor.awaitTermination(5L, TimeUnit.SECONDS); - if (config.getScheduledExecutorService() == null) { - houseKeepingExecutorService.shutdown(); - houseKeepingExecutorService.awaitTermination(5L, TimeUnit.SECONDS); - } - - final ExecutorService assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), "Hikari connection assassin", - config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); - try { - final long start = clockSource.currentTime(); - do { - softEvictConnections(); - abortActiveConnections(assassinExecutor); - } - while (getTotalConnections() > 0 && clockSource.elapsedMillis(start) < TimeUnit.SECONDS.toMillis(5)); - } finally { - assassinExecutor.shutdown(); - assassinExecutor.awaitTermination(5L, TimeUnit.SECONDS); - } - - poolElf.shutdownTimeoutExecutor(); - closeConnectionExecutor.shutdown(); - closeConnectionExecutor.awaitTermination(5L, TimeUnit.SECONDS); - } - finally { - logPoolState("After shutdown "); - - poolElf.unregisterMBeans(); - metricsTracker.close(); - } - } - - /** - * Evict a connection from the pool. - * - * @param proxyConnection the connection to evict - */ - public final void evictConnection(IHikariConnectionProxy proxyConnection) - { - closeConnection(proxyConnection.getPoolBagEntry(), "(connection evicted by user)"); - } - - /** - * Get the wrapped DataSource. - * - * @return the wrapped DataSource - */ - public final DataSource getDataSource() - { - return dataSource; - } - - public void setMetricRegistry(Object metricRegistry) - { - this.isRecordMetrics = metricRegistry != null; - if (isRecordMetrics) { - setMetricsTrackerFactory(new CodahaleMetricsTrackerFactory((MetricRegistry) metricRegistry)); - } - else { - setMetricsTrackerFactory(null); - } - } - - public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) - { - this.isRecordMetrics = metricsTrackerFactory != null; - if (isRecordMetrics) { - this.metricsTracker = metricsTrackerFactory.create(config.getPoolName(), getPoolStats()); - } - else { - this.metricsTracker = new MetricsTracker(); - } - } - - public void setHealthCheckRegistry(Object healthCheckRegistry) - { - if (healthCheckRegistry != null) { - CodahaleHealthChecker.registerHealthChecks(this, config, (HealthCheckRegistry) healthCheckRegistry); - } - } - - /** - * Log the current pool state at debug level. - * - * @param prefix an optional prefix to prepend the log message - */ - public final void logPoolState(String... prefix) - { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("{}pool {} stats (total={}, active={}, idle={}, waiting={})", - (prefix.length > 0 ? prefix[0] : ""), poolName, - getTotalConnections(), getActiveConnections(), getIdleConnections(), getThreadsAwaitingConnection()); - } - } - - /** {@inheritDoc} */ - @Override - public String toString() - { - return poolName; - } - - // *********************************************************************** - // IBagStateListener callback - // *********************************************************************** - - /** {@inheritDoc} */ - @Override - public Future addBagItem() - { - FutureTask future = new FutureTask<>(new Runnable() { - @Override - public void run() - { - long sleepBackoff = 200L; - final int minimumIdle = config.getMinimumIdle(); - final int maxPoolSize = config.getMaximumPoolSize(); - while (poolState == POOL_NORMAL && totalConnections.get() < maxPoolSize && getIdleConnections() <= minimumIdle && !addConnection()) { - // If we got into the loop, addConnection() failed, so we sleep and retry - quietlySleep(sleepBackoff); - sleepBackoff = Math.min(connectionTimeout / 2, (long) ((double) sleepBackoff * 1.5)); - } - } - }, true); - - addConnectionExecutor.execute(future); - return future; - } - - // *********************************************************************** - // HikariPoolMBean methods - // *********************************************************************** - - /** {@inheritDoc} */ - @Override - public final int getActiveConnections() - { - return connectionBag.getCount(STATE_IN_USE); - } - - /** {@inheritDoc} */ - @Override - public final int getIdleConnections() - { - return connectionBag.getCount(STATE_NOT_IN_USE); - } - - /** {@inheritDoc} */ - @Override - public final int getTotalConnections() - { - return connectionBag.size() - connectionBag.getCount(STATE_REMOVED); - } - - /** {@inheritDoc} */ - @Override - public final int getThreadsAwaitingConnection() - { - return connectionBag.getPendingQueue(); - } - - /** {@inheritDoc} */ - @Override - public void softEvictConnections() - { - for (PoolBagEntry bagEntry : connectionBag.values()) { - bagEntry.evicted = true; - if (connectionBag.reserve(bagEntry)) { - closeConnection(bagEntry, "(connection evicted by user)"); - } - } - } - - /** {@inheritDoc} */ - @Override - public final synchronized void suspendPool() - { - if (suspendResumeLock == SuspendResumeLock.FAUX_LOCK) { - throw new IllegalStateException("Pool " + poolName + " is not suspendable"); - } - else if (poolState != POOL_SUSPENDED) { - suspendResumeLock.suspend(); - poolState = POOL_SUSPENDED; - } - } - - /** {@inheritDoc} */ - @Override - public final synchronized void resumePool() - { - if (poolState == POOL_SUSPENDED) { - poolState = POOL_NORMAL; - fillPool(); - suspendResumeLock.resume(); - } - } - - // *********************************************************************** - // Package methods - // *********************************************************************** - - /** - * Permanently close the real (underlying) connection (eat any exception). - * - * @param bagEntry the connection to actually close - */ - void closeConnection(final PoolBagEntry bagEntry, final String closureReason) - { - final Connection connection = bagEntry.connection; - bagEntry.connection = null; - bagEntry.cancelMaxLifeTermination(); - if (connectionBag.remove(bagEntry)) { - final int tc = totalConnections.decrementAndGet(); - if (tc < 0) { - LOGGER.warn("{} - Internal accounting inconsistency, totalConnections={}", poolName, tc, new Exception()); - } - - closeConnectionExecutor.execute(new Runnable() { - @Override - public void run() { - poolElf.quietlyCloseConnection(connection, closureReason); - } - }); - } - } - - // *********************************************************************** - // Private methods - // *********************************************************************** - - /** - * Create and add a single connection to the pool. - */ - private boolean addConnection() - { - // Speculative increment of totalConnections with expectation of success - if (totalConnections.incrementAndGet() > config.getMaximumPoolSize()) { - totalConnections.decrementAndGet(); // Pool is maxed out, so undo speculative increment of totalConnections - lastConnectionFailure.set(new SQLException("Hikari pool " + poolName +" is at maximum capacity")); - return true; - } - - Connection connection = null; - try { - String username = config.getUsername(); - String password = config.getPassword(); - - connection = (username == null && password == null) ? dataSource.getConnection() : dataSource.getConnection(username, password); - poolElf.setupConnection(connection, connectionTimeout); - - connectionBag.add(new PoolBagEntry(connection, this)); - lastConnectionFailure.set(null); - LOGGER.debug("{} - Connection {} added to pool", poolName, connection); - - return true; - } - catch (Exception e) { - totalConnections.decrementAndGet(); // We failed, so undo speculative increment of totalConnections - lastConnectionFailure.set(e); - if (poolState == POOL_NORMAL) { - LOGGER.debug("{} - Connection attempt to database failed", poolName, e); - } - poolElf.quietlyCloseConnection(connection, "(exception during connection creation)"); - return false; - } - } - - /** - * Fill pool up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections. - */ - private void fillPool() - { - final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - totalConnections.get(), config.getMinimumIdle() - getIdleConnections()); - for (int i = 0; i < connectionsToAdd; i++) { - addBagItem(); - } - - if (connectionsToAdd > 0 && LOGGER.isDebugEnabled()) { - addConnectionExecutor.execute(new Runnable() { - @Override - public void run() { - logPoolState("After fill "); - } - }); - } - } - - /** - * Attempt to abort() active connections, or close() them. - */ - private void abortActiveConnections(final ExecutorService assassinExecutor) - { - for (PoolBagEntry bagEntry : connectionBag.values(STATE_IN_USE)) { - try { - bagEntry.aborted = bagEntry.evicted = true; - bagEntry.connection.abort(assassinExecutor); - } - catch (Throwable e) { - poolElf.quietlyCloseConnection(bagEntry.connection, "(connection aborted during shutdown)"); - } - finally { - bagEntry.connection = null; - if (connectionBag.remove(bagEntry)) { - totalConnections.decrementAndGet(); - } - } - } - } - - /** - * Fill the pool up to the minimum size. - */ - private void initializeConnections() - { - if (config.isInitializationFailFast()) { - try { - if (!addConnection()) { - throw lastConnectionFailure.getAndSet(null); - } - - ConnectionProxy connection = (ConnectionProxy) getConnection(); - connection.getPoolBagEntry().evicted = (config.getMinimumIdle() == 0); - connection.close(); - } - catch (Throwable e) { - try { - shutdown(); - } - catch (Throwable ex) { - e.addSuppressed(ex); - } - - throw new PoolInitializationException(e); - } - } - - fillPool(); - } - - private PoolStats getPoolStats() - { - return new PoolStats(TimeUnit.SECONDS.toMillis(1)) { - @Override - protected void update() { - this.pendingThreads = HikariPool.this.getThreadsAwaitingConnection(); - this.idleConnections = HikariPool.this.getIdleConnections(); - this.totalConnections = HikariPool.this.getTotalConnections(); - this.activeConnections = HikariPool.this.getActiveConnections(); - } - }; - } - - // *********************************************************************** - // Non-anonymous Inner-classes - // *********************************************************************** - - /** - * The house keeping task to retire idle connections. - */ - private class HouseKeeper implements Runnable - { - private volatile long previous = clockSource.currentTime(); - - @Override - public void run() - { - // refresh timeouts in case they changed via MBean - connectionTimeout = config.getConnectionTimeout(); - poolElf.setValidationTimeout(config.getValidationTimeout()); - leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold()); - - final long now = clockSource.currentTime(); - final long idleTimeout = config.getIdleTimeout(); - - // Detect retrograde time as well as forward leaps of unacceptable duration - if (now < previous || now > clockSource.plusMillis(previous, (2 * HOUSEKEEPING_PERIOD_MS))) { - LOGGER.warn("{} - Unusual system clock change detected, soft-evicting connections from pool.", poolName); - softEvictConnections(); - fillPool(); - return; - } - - previous = now; - - logPoolState("Before cleanup "); - for (PoolBagEntry bagEntry : connectionBag.values(STATE_NOT_IN_USE)) { - if (connectionBag.reserve(bagEntry)) { - if (bagEntry.evicted) { - closeConnection(bagEntry, "(connection evicted)"); - } - else if (idleTimeout > 0L && clockSource.elapsedMillis(bagEntry.lastAccess, now) > idleTimeout) { - closeConnection(bagEntry, "(connection passed idleTimeout)"); - } - else { - connectionBag.unreserve(bagEntry); - } - } - } - - logPoolState("After cleanup "); - - fillPool(); // Try to maintain minimum connections - } - } -} +/* + * Copyright (C) 2013,2014 Brett Wooldridge + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zaxxer.hikari.pool; + +import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.STATE_IN_USE; +import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.STATE_NOT_IN_USE; +import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.STATE_REMOVED; +import static com.zaxxer.hikari.util.UtilityElf.createThreadPoolExecutor; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariPoolMXBean; +import com.zaxxer.hikari.metrics.CodahaleHealthChecker; +import com.zaxxer.hikari.metrics.CodahaleMetricsTrackerFactory; +import com.zaxxer.hikari.metrics.MetricsTracker; +import com.zaxxer.hikari.metrics.MetricsTracker.MetricsContext; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.metrics.PoolStats; +import com.zaxxer.hikari.proxy.ConnectionProxy; +import com.zaxxer.hikari.proxy.IHikariConnectionProxy; +import com.zaxxer.hikari.proxy.ProxyFactory; +import com.zaxxer.hikari.util.ClockSource; +import com.zaxxer.hikari.util.ConcurrentBag; +import com.zaxxer.hikari.util.ConcurrentBag.IBagStateListener; +import com.zaxxer.hikari.util.DefaultThreadFactory; +import com.zaxxer.hikari.util.PropertyElf; + +/** + * This is the primary connection pool class that provides the basic + * pooling behavior for HikariCP. + * + * @author Brett Wooldridge + */ +public class HikariPool implements HikariPoolMXBean, IBagStateListener +{ + final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private static final ClockSource clockSource = ClockSource.INSTANCE; + + private final long ALIVE_BYPASS_WINDOW_MS = Long.getLong("com.zaxxer.hikari.aliveBypassWindow", TimeUnit.SECONDS.toMillis(1)); + private final long HOUSEKEEPING_PERIOD_MS = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", TimeUnit.SECONDS.toMillis(30)); + + private static final int POOL_NORMAL = 0; + private static final int POOL_SUSPENDED = 1; + private static final int POOL_SHUTDOWN = 2; + + final PoolElf poolElf; + final HikariConfig config; + final ConcurrentBag connectionBag; + final ScheduledThreadPoolExecutor houseKeepingExecutorService; + + private final AtomicInteger totalConnections; + private final ThreadPoolExecutor addConnectionExecutor; + private final ThreadPoolExecutor closeConnectionExecutor; + + private volatile int poolState; + private long connectionTimeout; + + private final String poolName; + private final LeakTask leakTask; + private final DataSource dataSource; + private final SuspendResumeLock suspendResumeLock; + private final AtomicReference lastConnectionFailure; + + private volatile MetricsTracker metricsTracker; + private boolean isRecordMetrics; + + /** + * Construct a HikariPool with the specified configuration. + * + * @param config a HikariConfig instance + */ + public HikariPool(final HikariConfig config) + { + this.config = config; + + this.poolElf = new PoolElf(config); + this.dataSource = poolElf.initializeDataSource(); + + this.poolName = config.getPoolName(); + this.connectionBag = new ConcurrentBag<>(this); + this.totalConnections = new AtomicInteger(); + this.connectionTimeout = config.getConnectionTimeout(); + this.lastConnectionFailure = new AtomicReference<>(); + this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock(true) : SuspendResumeLock.FAUX_LOCK; + + this.addConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), "Hikari connection filler (pool " + poolName + ")", config.getThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); + this.closeConnectionExecutor = createThreadPoolExecutor(4, "Hikari connection closer (pool " + poolName + ")", config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); + + if (config.getScheduledExecutorService() == null) { + ThreadFactory threadFactory = config.getThreadFactory() != null ? config.getThreadFactory() : new DefaultThreadFactory("Hikari housekeeper (pool " + poolName + ")", true); + this.houseKeepingExecutorService = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy()); + this.houseKeepingExecutorService.scheduleAtFixedRate(new HouseKeeper(), HOUSEKEEPING_PERIOD_MS, HOUSEKEEPING_PERIOD_MS, TimeUnit.MILLISECONDS); + this.houseKeepingExecutorService.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.houseKeepingExecutorService.setRemoveOnCancelPolicy(true); + } + else { + this.houseKeepingExecutorService = config.getScheduledExecutorService(); + } + + this.leakTask = new LeakTask(config.getLeakDetectionThreshold(), houseKeepingExecutorService); + + if (config.getMetricsTrackerFactory() != null) { + setMetricsTrackerFactory(config.getMetricsTrackerFactory()); + } + else { + setMetricRegistry(config.getMetricRegistry()); + } + + setHealthCheckRegistry(config.getHealthCheckRegistry()); + + poolElf.registerMBeans(this); + + PropertyElf.flushCaches(); + + initializeConnections(); + } + + /** + * Get a connection from the pool, or timeout after connectionTimeout milliseconds. + * + * @return a java.sql.Connection instance + * @throws SQLException thrown if a timeout occurs trying to obtain a connection + */ + public final Connection getConnection() throws SQLException + { + return getConnection(connectionTimeout); + } + + /** + * Get a connection from the pool, or timeout after the specified number of milliseconds. + * + * @param hardTimeout the maximum time to wait for a connection from the pool + * @return a java.sql.Connection instance + * @throws SQLException thrown if a timeout occurs trying to obtain a connection + */ + public final Connection getConnection(final long hardTimeout) throws SQLException + { + suspendResumeLock.acquire(); + final long startTime = clockSource.currentTime(); + + try { + long timeout = hardTimeout; + final MetricsContext metricsContext = (isRecordMetrics ? metricsTracker.recordConnectionRequest() : MetricsTracker.NO_CONTEXT); + do { + final PoolBagEntry bagEntry = connectionBag.borrow(timeout, TimeUnit.MILLISECONDS); + if (bagEntry == null) { + break; // We timed out... break and throw exception + } + + final long now = clockSource.currentTime(); + if (bagEntry.evicted || (clockSource.elapsedMillis(bagEntry.lastAccess, now) > ALIVE_BYPASS_WINDOW_MS && !poolElf.isConnectionAlive(bagEntry.connection, lastConnectionFailure))) { + closeConnection(bagEntry, "(connection evicted or dead)"); // Throw away the dead connection and try again + timeout = hardTimeout - clockSource.elapsedMillis(startTime, now); + } + else { + metricsContext.setConnectionLastOpen(bagEntry, now); + metricsContext.stop(); + return ProxyFactory.getProxyConnection(bagEntry, leakTask.start(bagEntry), now); + } + } + while (timeout > 0L); + } + catch (InterruptedException e) { + throw new SQLException(poolName + " - Interrupted during connection acquisition", e); + } + finally { + suspendResumeLock.release(); + } + + logPoolState("Timeout failure "); + String sqlState = null; + final Throwable originalException = lastConnectionFailure.getAndSet(null); + if (originalException instanceof SQLException) { + sqlState = ((SQLException) originalException).getSQLState(); + } + throw new SQLTimeoutException(poolName + " - Cannot acquire connection, Timeout after " + clockSource.elapsedMillis(startTime) + "ms.", sqlState, originalException); + } + + /** + * Release a connection back to the pool, or permanently close it if it is broken. + * + * @param bagEntry the PoolBagEntry to release back to the pool + */ + public final void releaseConnection(final PoolBagEntry bagEntry) + { + metricsTracker.recordConnectionUsage(bagEntry); + + if (bagEntry.evicted) { + closeConnection(bagEntry, "(connection broken or evicted)"); + } + else { + connectionBag.requite(bagEntry); + } + } + + /** + * Shutdown the pool, closing all idle connections and aborting or closing + * active connections. + * + * @throws InterruptedException thrown if the thread is interrupted during shutdown + */ + public final synchronized void shutdown() throws InterruptedException + { + try { + poolState = POOL_SHUTDOWN; + + LOGGER.info("{} - is closing down.", poolName); + logPoolState("Before closing "); + + connectionBag.close(); + softEvictConnections(); + addConnectionExecutor.shutdownNow(); + addConnectionExecutor.awaitTermination(5L, TimeUnit.SECONDS); + if (config.getScheduledExecutorService() == null) { + houseKeepingExecutorService.shutdown(); + houseKeepingExecutorService.awaitTermination(5L, TimeUnit.SECONDS); + } + + final ExecutorService assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), "Hikari connection assassin", + config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); + try { + final long start = clockSource.currentTime(); + do { + softEvictConnections(); + abortActiveConnections(assassinExecutor); + } + while (getTotalConnections() > 0 && clockSource.elapsedMillis(start) < TimeUnit.SECONDS.toMillis(5)); + } finally { + assassinExecutor.shutdown(); + assassinExecutor.awaitTermination(5L, TimeUnit.SECONDS); + } + + poolElf.shutdownTimeoutExecutor(); + closeConnectionExecutor.shutdown(); + closeConnectionExecutor.awaitTermination(5L, TimeUnit.SECONDS); + } + finally { + logPoolState("After closing "); + + poolElf.unregisterMBeans(); + metricsTracker.close(); + } + } + + /** + * Evict a connection from the pool. + * + * @param proxyConnection the connection to evict + */ + public final void evictConnection(IHikariConnectionProxy proxyConnection) + { + closeConnection(proxyConnection.getPoolBagEntry(), "(connection evicted by user)"); + } + + /** + * Get the wrapped DataSource. + * + * @return the wrapped DataSource + */ + public final DataSource getDataSource() + { + return dataSource; + } + + public void setMetricRegistry(Object metricRegistry) + { + this.isRecordMetrics = metricRegistry != null; + if (isRecordMetrics) { + setMetricsTrackerFactory(new CodahaleMetricsTrackerFactory((MetricRegistry) metricRegistry)); + } + else { + setMetricsTrackerFactory(null); + } + } + + public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) + { + this.isRecordMetrics = metricsTrackerFactory != null; + if (isRecordMetrics) { + this.metricsTracker = metricsTrackerFactory.create(config.getPoolName(), getPoolStats()); + } + else { + this.metricsTracker = new MetricsTracker(); + } + } + + public void setHealthCheckRegistry(Object healthCheckRegistry) + { + if (healthCheckRegistry != null) { + CodahaleHealthChecker.registerHealthChecks(this, config, (HealthCheckRegistry) healthCheckRegistry); + } + } + + /** + * Log the current pool state at debug level. + * + * @param prefix an optional prefix to prepend the log message + */ + public final void logPoolState(String... prefix) + { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("{}pool {} stats (total={}, active={}, idle={}, waiting={})", + (prefix.length > 0 ? prefix[0] : ""), poolName, + getTotalConnections(), getActiveConnections(), getIdleConnections(), getThreadsAwaitingConnection()); + } + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + return poolName; + } + + // *********************************************************************** + // IBagStateListener callback + // *********************************************************************** + + /** {@inheritDoc} */ + @Override + public Future addBagItem() + { + FutureTask future = new FutureTask<>(new Runnable() { + @Override + public void run() + { + long sleepBackoff = 200L; + final int minimumIdle = config.getMinimumIdle(); + final int maxPoolSize = config.getMaximumPoolSize(); + while (poolState == POOL_NORMAL && totalConnections.get() < maxPoolSize && getIdleConnections() <= minimumIdle && !addConnection()) { + // If we got into the loop, addConnection() failed, so we sleep and retry + quietlySleep(sleepBackoff); + sleepBackoff = Math.min(connectionTimeout / 2, (long) ((double) sleepBackoff * 1.5)); + } + } + }, true); + + addConnectionExecutor.execute(future); + return future; + } + + // *********************************************************************** + // HikariPoolMBean methods + // *********************************************************************** + + /** {@inheritDoc} */ + @Override + public final int getActiveConnections() + { + return connectionBag.getCount(STATE_IN_USE); + } + + /** {@inheritDoc} */ + @Override + public final int getIdleConnections() + { + return connectionBag.getCount(STATE_NOT_IN_USE); + } + + /** {@inheritDoc} */ + @Override + public final int getTotalConnections() + { + return connectionBag.size() - connectionBag.getCount(STATE_REMOVED); + } + + /** {@inheritDoc} */ + @Override + public final int getThreadsAwaitingConnection() + { + return connectionBag.getPendingQueue(); + } + + /** {@inheritDoc} */ + @Override + public void softEvictConnections() + { + for (PoolBagEntry bagEntry : connectionBag.values()) { + bagEntry.evicted = true; + if (connectionBag.reserve(bagEntry)) { + closeConnection(bagEntry, "(connection evicted by user)"); + } + } + } + + /** {@inheritDoc} */ + @Override + public final synchronized void suspendPool() + { + if (suspendResumeLock == SuspendResumeLock.FAUX_LOCK) { + throw new IllegalStateException(poolName + " - is not suspendable"); + } + else if (poolState != POOL_SUSPENDED) { + suspendResumeLock.suspend(); + poolState = POOL_SUSPENDED; + } + } + + /** {@inheritDoc} */ + @Override + public final synchronized void resumePool() + { + if (poolState == POOL_SUSPENDED) { + poolState = POOL_NORMAL; + fillPool(); + suspendResumeLock.resume(); + } + } + + // *********************************************************************** + // Package methods + // *********************************************************************** + + /** + * Permanently close the real (underlying) connection (eat any exception). + * + * @param bagEntry the connection to actually close + */ + void closeConnection(final PoolBagEntry bagEntry, final String closureReason) + { + final Connection connection = bagEntry.connection; + bagEntry.connection = null; + bagEntry.cancelMaxLifeTermination(); + if (connectionBag.remove(bagEntry)) { + final int tc = totalConnections.decrementAndGet(); + if (tc < 0) { + LOGGER.warn("{} - Internal accounting inconsistency, totalConnections={}", poolName, tc, new Exception()); + } + + closeConnectionExecutor.execute(new Runnable() { + @Override + public void run() { + poolElf.quietlyCloseConnection(connection, closureReason); + } + }); + } + } + + // *********************************************************************** + // Private methods + // *********************************************************************** + + /** + * Create and add a single connection to the pool. + */ + private boolean addConnection() + { + // Speculative increment of totalConnections with expectation of success + if (totalConnections.incrementAndGet() > config.getMaximumPoolSize()) { + totalConnections.decrementAndGet(); // Pool is maxed out, so undo speculative increment of totalConnections + lastConnectionFailure.set(new SQLException(poolName +" - is at maximum capacity")); + return true; + } + + Connection connection = null; + try { + String username = config.getUsername(); + String password = config.getPassword(); + + connection = (username == null && password == null) ? dataSource.getConnection() : dataSource.getConnection(username, password); + poolElf.setupConnection(connection, connectionTimeout); + + connectionBag.add(new PoolBagEntry(connection, this)); + lastConnectionFailure.set(null); + LOGGER.debug("{} - Added connection {}", poolName, connection); + + return true; + } + catch (Exception e) { + totalConnections.decrementAndGet(); // We failed, so undo speculative increment of totalConnections + lastConnectionFailure.set(e); + if (poolState == POOL_NORMAL) { + LOGGER.debug("{} - Cannot acquire connection from data source", poolName, e); + } + poolElf.quietlyCloseConnection(connection, "(exception during connection creation)"); + return false; + } + } + + /** + * Fill pool up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections. + */ + private void fillPool() + { + final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - totalConnections.get(), config.getMinimumIdle() - getIdleConnections()); + for (int i = 0; i < connectionsToAdd; i++) { + addBagItem(); + } + + if (connectionsToAdd > 0 && LOGGER.isDebugEnabled()) { + addConnectionExecutor.execute(new Runnable() { + @Override + public void run() { + logPoolState("After fill "); + } + }); + } + } + + /** + * Attempt to abort() active connections, or close() them. + */ + private void abortActiveConnections(final ExecutorService assassinExecutor) + { + for (PoolBagEntry bagEntry : connectionBag.values(STATE_IN_USE)) { + try { + bagEntry.aborted = bagEntry.evicted = true; + bagEntry.connection.abort(assassinExecutor); + } + catch (Throwable e) { + poolElf.quietlyCloseConnection(bagEntry.connection, "(connection aborted during shutdown)"); + } + finally { + bagEntry.connection = null; + if (connectionBag.remove(bagEntry)) { + totalConnections.decrementAndGet(); + } + } + } + } + + /** + * Fill the pool up to the minimum size. + */ + private void initializeConnections() + { + if (config.isInitializationFailFast()) { + try { + if (!addConnection()) { + throw lastConnectionFailure.getAndSet(null); + } + + ConnectionProxy connection = (ConnectionProxy) getConnection(); + connection.getPoolBagEntry().evicted = (config.getMinimumIdle() == 0); + connection.close(); + } + catch (Throwable e) { + try { + shutdown(); + } + catch (Throwable ex) { + e.addSuppressed(ex); + } + + throw new PoolInitializationException(e); + } + } + + fillPool(); + } + + private PoolStats getPoolStats() + { + return new PoolStats(TimeUnit.SECONDS.toMillis(1)) { + @Override + protected void update() { + this.pendingThreads = HikariPool.this.getThreadsAwaitingConnection(); + this.idleConnections = HikariPool.this.getIdleConnections(); + this.totalConnections = HikariPool.this.getTotalConnections(); + this.activeConnections = HikariPool.this.getActiveConnections(); + } + }; + } + + // *********************************************************************** + // Non-anonymous Inner-classes + // *********************************************************************** + + /** + * The house keeping task to retire idle connections. + */ + private class HouseKeeper implements Runnable + { + private volatile long previous = clockSource.currentTime(); + + @Override + public void run() + { + // refresh timeouts in case they changed via MBean + connectionTimeout = config.getConnectionTimeout(); + poolElf.setValidationTimeout(config.getValidationTimeout()); + leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold()); + + final long now = clockSource.currentTime(); + final long idleTimeout = config.getIdleTimeout(); + + // Detect retrograde time as well as forward leaps of unacceptable duration + if (now < previous || now > clockSource.plusMillis(previous, (2 * HOUSEKEEPING_PERIOD_MS))) { + LOGGER.warn("{} - Unusual system clock change detected, soft-evicting connections from pool.", poolName); + softEvictConnections(); + fillPool(); + return; + } + + previous = now; + + logPoolState("Before cleanup "); + for (PoolBagEntry bagEntry : connectionBag.values(STATE_NOT_IN_USE)) { + if (connectionBag.reserve(bagEntry)) { + if (bagEntry.evicted) { + closeConnection(bagEntry, "(connection evicted)"); + } + else if (idleTimeout > 0L && clockSource.elapsedMillis(bagEntry.lastAccess, now) > idleTimeout) { + closeConnection(bagEntry, "(connection passed idleTimeout)"); + } + else { + connectionBag.unreserve(bagEntry); + } + } + } + + logPoolState("After cleanup "); + + fillPool(); // Try to maintain minimum connections + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/PoolElf.java b/src/main/java/com/zaxxer/hikari/pool/PoolElf.java index a530e978..b0a2e6f7 100644 --- a/src/main/java/com/zaxxer/hikari/pool/PoolElf.java +++ b/src/main/java/com/zaxxer/hikari/pool/PoolElf.java @@ -1,524 +1,520 @@ -package com.zaxxer.hikari.pool; - -import static com.zaxxer.hikari.util.UtilityElf.createInstance; - -import java.lang.management.ManagementFactory; -import java.lang.reflect.Field; -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.TimeUnit; -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.util.DefaultThreadFactory; -import com.zaxxer.hikari.util.DriverDataSource; -import com.zaxxer.hikari.util.PropertyElf; - -public final class PoolElf -{ - private static final Logger LOGGER = LoggerFactory.getLogger(PoolElf.class); - 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 transactionIsolation; - private long validationTimeout; - private int isNetworkTimeoutSupported; - private int isQueryTimeoutSupported; - private Executor netTimeoutExecutor; - - private final HikariConfig config; - private final String poolName; - private final String catalog; - private final Boolean isReadOnly; - private final boolean isAutoCommit; - private final boolean isUseJdbc4Validation; - private final boolean isIsolateInternalQueries; - - private volatile boolean isValidChecked; - private volatile boolean isValidSupported; - - public PoolElf(final HikariConfig configuration) - { - this.config = configuration; - - this.networkTimeout = -1; - this.catalog = config.getCatalog(); - this.isReadOnly = config.isReadOnly(); - this.isAutoCommit = config.isAutoCommit(); - this.validationTimeout = config.getValidationTimeout(); - this.transactionIsolation = getTransactionIsolation(config.getTransactionIsolation()); - - this.isValidSupported = true; - this.isQueryTimeoutSupported = UNINITIALIZED; - this.isNetworkTimeoutSupported = UNINITIALIZED; - this.isUseJdbc4Validation = config.getConnectionTestQuery() == null; - this.isIsolateInternalQueries = config.isIsolateInternalQueries(); - this.poolName = config.getPoolName(); - } - - /** - * Close connection and eat any exception. - * - * @param connection the connection to close - * @param closureReason the reason the connection was closed (if known) - */ - public void quietlyCloseConnection(final Connection connection, final String closureReason) - { - try { - if (connection == null || connection.isClosed()) { - return; - } - - LOGGER.debug("{} - Closing connection {}: {}", poolName, connection, closureReason); - try { - setNetworkTimeout(connection, TimeUnit.SECONDS.toMillis(15)); - } - finally { - // continue with the close even if setNetworkTimeout() throws (due to driver poorly behaving drivers) - connection.close(); - } - } - catch (Throwable e) { - LOGGER.debug("{} - Closing connection {} failed", poolName, connection, e); - } - } - - /** - * Get the int value of a transaction isolation level by name. - * - * @param transactionIsolationName the name of the transaction isolation level - * @return the int value of the isolation level or -1 - */ - public static int getTransactionIsolation(final String transactionIsolationName) - { - if (transactionIsolationName != null) { - try { - Field field = Connection.class.getField(transactionIsolationName); - return field.getInt(null); - } - catch (Exception e) { - throw new IllegalArgumentException("Invalid transaction isolation value: " + transactionIsolationName); - } - } - - return -1; - } - - /** - * Create/initialize the underlying DataSource. - * - * @return a DataSource instance - */ - DataSource 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, config.getConnectionTimeout()); - createNetworkTimeoutExecutor(dataSource, dsClassName, jdbcUrl); - } - - return dataSource; - } - - /** - * Setup a connection initial state. - * - * @param connection a Connection - * @param connectionTimeout the connection timeout - * @throws SQLException thrown from driver - */ - void setupConnection(final Connection connection, final long connectionTimeout) throws SQLException - { - if (isUseJdbc4Validation && !isJdbc4ValidationSupported(connection)) { - throw new SQLException("Connection.isValid() method is not supported, connection test query must be configured"); - } - - networkTimeout = getAndSetNetworkTimeout(connection, connectionTimeout); - transactionIsolation = (transactionIsolation < 0 ? connection.getTransactionIsolation() : transactionIsolation); - - connection.setAutoCommit(isAutoCommit); - if (isReadOnly != null) { - connection.setReadOnly(isReadOnly); - } - - if (transactionIsolation != connection.getTransactionIsolation()) { - connection.setTransactionIsolation(transactionIsolation); - } - - if (catalog != null) { - connection.setCatalog(catalog); - } - - executeSql(connection, config.getConnectionInitSql(), isAutoCommit); - - setNetworkTimeout(connection, networkTimeout); - } - - /** - * Check whether the connection is alive or not. - * - * @param connection the connection to test - * @param lastConnectionFailure last connection failure - * @return true if the connection is alive, false if it is not alive or we timed out - */ - boolean isConnectionAlive(final Connection connection, AtomicReference lastConnectionFailure) - { - try { - int timeoutSec = (int) TimeUnit.MILLISECONDS.toSeconds(validationTimeout); - - if (isUseJdbc4Validation) { - return connection.isValid(timeoutSec); - } - - final int originalTimeout = getAndSetNetworkTimeout(connection, validationTimeout); - - try (Statement statement = connection.createStatement()) { - setQueryTimeout(statement, timeoutSec); - if (statement.execute(config.getConnectionTestQuery())) { - statement.getResultSet().close(); - } - } - - if (isIsolateInternalQueries && !isAutoCommit) { - connection.rollback(); - } - - setNetworkTimeout(connection, originalTimeout); - - return true; - } - catch (SQLException e) { - lastConnectionFailure.set(e); - LOGGER.warn("{} - Connection {} failed alive test with exception {}", poolName, connection, e.getMessage()); - return false; - } - } - - void resetConnectionState(final PoolBagEntry poolEntry) throws SQLException - { - int resetBits = 0; - - if (isReadOnly != null && poolEntry.isReadOnly != isReadOnly) { - poolEntry.connection.setReadOnly(isReadOnly); - resetBits |= 0b00001; - } - - if (poolEntry.isAutoCommit != isAutoCommit) { - poolEntry.connection.setAutoCommit(isAutoCommit); - resetBits |= 0b00010; - } - - if (poolEntry.transactionIsolation != transactionIsolation) { - poolEntry.connection.setTransactionIsolation(transactionIsolation); - resetBits |= 0b00100; - } - - final String currentCatalog = poolEntry.catalog; - if ((currentCatalog != null && !currentCatalog.equals(catalog)) || (currentCatalog == null && catalog != null)) { - poolEntry.connection.setCatalog(catalog); - resetBits |= 0b01000; - } - - if (poolEntry.networkTimeout != networkTimeout) { - setNetworkTimeout(poolEntry.connection, networkTimeout); - resetBits |= 0b10000; - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("{} - Reset {} on connection {}", resetBits != 0 ? stringFromResetBits(resetBits) : "nothing", poolEntry.connection); - } - } - - void resetPoolEntry(final PoolBagEntry poolEntry) - { - if (isReadOnly != null) { - poolEntry.setReadOnly(isReadOnly); - } - poolEntry.setCatalog(catalog); - poolEntry.setAutoCommit(isAutoCommit); - poolEntry.setNetworkTimeout(networkTimeout); - poolEntry.setTransactionIsolation(transactionIsolation); - } - - void setValidationTimeout(final long validationTimeout) - { - this.validationTimeout = validationTimeout; - } - - /** - * Register MBeans for HikariConfig and HikariPool. - * - * @param pool a HikariPool instance - */ - void registerMBeans(final HikariPool pool) - { - 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(pool, beanPoolName); - } - else { - LOGGER.error("{} - You cannot use the same pool name for separate pool instances.", poolName); - } - } - catch (Exception e) { - LOGGER.warn("{} - Unable 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("{} - Unable to unregister management beans.", poolName, e); - } - } - - void shutdownTimeoutExecutor() - { - if (netTimeoutExecutor != null && netTimeoutExecutor instanceof ThreadPoolExecutor) { - ((ThreadPoolExecutor) netTimeoutExecutor).shutdownNow(); - } - } - - /** - * Return true if the driver appears to be JDBC 4.0 compliant. - * - * @param connection a Connection to check - * @return true if JDBC 4.1 compliance, false otherwise - */ - private boolean isJdbc4ValidationSupported(final Connection connection) - { - if (!isValidChecked) { - try { - // We don't care how long the wait actually is here, just whether it returns without exception. This - // call will throw various exceptions in the case of a non-JDBC 4.0 compliant driver - connection.isValid(1); - } - catch (Throwable e) { - isValidSupported = false; - LOGGER.debug("{} - Connection.isValid() is not supported ({})", poolName, e.getMessage()); - } - - isValidChecked = true; - } - - return isValidSupported; - } - - /** - * 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.debug("{} - Statement.setQueryTimeout() is not supported ({})", poolName, e.getMessage()); - } - } - } - } - - /** - * Set the network timeout, if isUseNetworkTimeout is true 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.debug("{} - Connection.setNetworkTimeout() is not supported ({})", poolName, e.getMessage()); - } - } - } - - return 0; - } - - /** - * Set the network timeout, if isUseNetworkTimeout is true 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 isAutoCommit 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 isAutoCommit) throws SQLException - { - if (sql != null) { - try (Statement statement = connection.createStatement()) { - if (statement.execute(sql)) { - statement.getResultSet().close(); - } - - if (!isAutoCommit) { - connection.commit(); - } - } - } - } - - 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() != null ? config.getThreadFactory() : new DefaultThreadFactory("Hikari JDBC-timeout executor", true); - ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(threadFactory); - executor.allowCoreThreadTimeOut(true); - executor.setKeepAliveTime(15, TimeUnit.SECONDS); - netTimeoutExecutor = executor; - } - } - - private static class SynchronousExecutor implements Executor - { - /** {@inheritDoc} */ - @Override - public void execute(Runnable command) - { - try { - command.run(); - } - catch (Throwable t) { - LOGGER.debug("Exception executing {}", command, t); - } - } - } - - /** - * 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) TimeUnit.MILLISECONDS.toSeconds(Math.max(1000L, connectionTimeout))); - } - catch (SQLException e) { - LOGGER.warn("{} - Unable to set DataSource login timeout", poolName, e); - } - } - } - - /** - * 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(); - } -} +package com.zaxxer.hikari.pool; + +import static com.zaxxer.hikari.util.UtilityElf.createInstance; + +import java.lang.management.ManagementFactory; +import java.lang.reflect.Field; +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.TimeUnit; +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.util.DefaultThreadFactory; +import com.zaxxer.hikari.util.DriverDataSource; +import com.zaxxer.hikari.util.PropertyElf; + +public final class PoolElf +{ + private static final Logger LOGGER = LoggerFactory.getLogger(PoolElf.class); + 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 transactionIsolation; + private long validationTimeout; + private int isNetworkTimeoutSupported; + private int isQueryTimeoutSupported; + private Executor netTimeoutExecutor; + + private final HikariConfig config; + private final String poolName; + private final String catalog; + private final Boolean isReadOnly; + private final boolean isAutoCommit; + private final boolean isUseJdbc4Validation; + private final boolean isIsolateInternalQueries; + + private volatile boolean isValidChecked; + private volatile boolean isValidSupported; + + public PoolElf(final HikariConfig configuration) + { + this.config = configuration; + + this.networkTimeout = -1; + this.catalog = config.getCatalog(); + this.isReadOnly = config.isReadOnly(); + this.isAutoCommit = config.isAutoCommit(); + this.validationTimeout = config.getValidationTimeout(); + this.transactionIsolation = getTransactionIsolation(config.getTransactionIsolation()); + + this.isValidSupported = true; + this.isQueryTimeoutSupported = UNINITIALIZED; + this.isNetworkTimeoutSupported = UNINITIALIZED; + this.isUseJdbc4Validation = config.getConnectionTestQuery() == null; + this.isIsolateInternalQueries = config.isIsolateInternalQueries(); + this.poolName = config.getPoolName(); + } + + /** + * Close connection and eat any exception. + * + * @param connection the connection to close + * @param closureReason the reason the connection was closed (if known) + */ + public void quietlyCloseConnection(final Connection connection, final String closureReason) + { + try { + if (connection == null || connection.isClosed()) { + return; + } + + LOGGER.debug("{} - Closing connection {}: {}", poolName, connection, closureReason); + try { + setNetworkTimeout(connection, TimeUnit.SECONDS.toMillis(15)); + } + finally { + // continue with the close even if setNetworkTimeout() throws (due to driver poorly behaving drivers) + connection.close(); + } + } + catch (Throwable e) { + LOGGER.debug("{} - Closing connection {} failed", poolName, connection, e); + } + } + + /** + * Get the int value of a transaction isolation level by name. + * + * @param transactionIsolationName the name of the transaction isolation level + * @return the int value of the isolation level or -1 + */ + public static int getTransactionIsolation(final String transactionIsolationName) + { + if (transactionIsolationName != null) { + try { + Field field = Connection.class.getField(transactionIsolationName); + return field.getInt(null); + } + catch (Exception e) { + throw new IllegalArgumentException(poolName + " - Invalid transaction isolation value: " + transactionIsolationName); + } + } + + return -1; + } + + /** + * Create/initialize the underlying DataSource. + * + * @return a DataSource instance + */ + DataSource 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, config.getConnectionTimeout()); + createNetworkTimeoutExecutor(dataSource, dsClassName, jdbcUrl); + } + + return dataSource; + } + + /** + * Setup a connection initial state. + * + * @param connection a Connection + * @param connectionTimeout the connection timeout + * @throws SQLException thrown from driver + */ + void setupConnection(final Connection connection, final long connectionTimeout) throws SQLException + { + if (isUseJdbc4Validation && !isJdbc4ValidationSupported(connection)) { + throw new SQLException("Connection.isValid() is not supported, connection test query must be configured"); + } + + networkTimeout = getAndSetNetworkTimeout(connection, connectionTimeout); + transactionIsolation = (transactionIsolation < 0 ? connection.getTransactionIsolation() : transactionIsolation); + + connection.setAutoCommit(isAutoCommit); + if (isReadOnly != null) { + connection.setReadOnly(isReadOnly); + } + + if (transactionIsolation != connection.getTransactionIsolation()) { + connection.setTransactionIsolation(transactionIsolation); + } + + if (catalog != null) { + connection.setCatalog(catalog); + } + + executeSql(connection, config.getConnectionInitSql(), isAutoCommit); + + setNetworkTimeout(connection, networkTimeout); + } + + /** + * Check whether the connection is alive or not. + * + * @param connection the connection to test + * @param lastConnectionFailure last connection failure + * @return true if the connection is alive, false if it is not alive or we timed out + */ + boolean isConnectionAlive(final Connection connection, AtomicReference lastConnectionFailure) + { + try { + int timeoutSec = (int) TimeUnit.MILLISECONDS.toSeconds(validationTimeout); + + if (isUseJdbc4Validation) { + return connection.isValid(timeoutSec); + } + + final int originalTimeout = getAndSetNetworkTimeout(connection, validationTimeout); + + try (Statement statement = connection.createStatement()) { + setQueryTimeout(statement, timeoutSec); + statement.execute(config.getConnectionTestQuery()); + } + + if (isIsolateInternalQueries && !isAutoCommit) { + connection.rollback(); + } + + setNetworkTimeout(connection, originalTimeout); + + return true; + } + catch (SQLException e) { + lastConnectionFailure.set(e); + LOGGER.warn("{} - Connection {} failed alive test with exception {}", poolName, connection, e.getMessage()); + return false; + } + } + + void resetConnectionState(final PoolBagEntry poolEntry) throws SQLException + { + int resetBits = 0; + + if (isReadOnly != null && poolEntry.isReadOnly != isReadOnly) { + poolEntry.connection.setReadOnly(isReadOnly); + resetBits |= 0b00001; + } + + if (poolEntry.isAutoCommit != isAutoCommit) { + poolEntry.connection.setAutoCommit(isAutoCommit); + resetBits |= 0b00010; + } + + if (poolEntry.transactionIsolation != transactionIsolation) { + poolEntry.connection.setTransactionIsolation(transactionIsolation); + resetBits |= 0b00100; + } + + final String currentCatalog = poolEntry.catalog; + if ((currentCatalog != null && !currentCatalog.equals(catalog)) || (currentCatalog == null && catalog != null)) { + poolEntry.connection.setCatalog(catalog); + resetBits |= 0b01000; + } + + if (poolEntry.networkTimeout != networkTimeout) { + setNetworkTimeout(poolEntry.connection, networkTimeout); + resetBits |= 0b10000; + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("{} - Reset {} on connection {}", resetBits != 0 ? stringFromResetBits(resetBits) : "nothing", poolEntry.connection); + } + } + + void resetPoolEntry(final PoolBagEntry poolEntry) + { + if (isReadOnly != null) { + poolEntry.setReadOnly(isReadOnly); + } + poolEntry.setCatalog(catalog); + poolEntry.setAutoCommit(isAutoCommit); + poolEntry.setNetworkTimeout(networkTimeout); + poolEntry.setTransactionIsolation(transactionIsolation); + } + + void setValidationTimeout(final long validationTimeout) + { + this.validationTimeout = validationTimeout; + } + + /** + * Register MBeans for HikariConfig and HikariPool. + * + * @param pool a HikariPool instance + */ + void registerMBeans(final HikariPool pool) + { + 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(pool, beanPoolName); + } + else { + LOGGER.error("{} - You cannot use the same pool name for separate pool instances.", poolName); + } + } + catch (Exception e) { + LOGGER.warn("{} - Unable 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("{} - Unable to unregister management beans.", poolName, e); + } + } + + void shutdownTimeoutExecutor() + { + if (netTimeoutExecutor != null && netTimeoutExecutor instanceof ThreadPoolExecutor) { + ((ThreadPoolExecutor) netTimeoutExecutor).shutdownNow(); + } + } + + /** + * Return true if the driver appears to be JDBC 4.0 compliant. + * + * @param connection a Connection to check + * @return true if JDBC 4.1 compliance, false otherwise + */ + private boolean isJdbc4ValidationSupported(final Connection connection) + { + if (!isValidChecked) { + try { + // We don't care how long the wait actually is here, just whether it returns without exception. This + // call will throw various exceptions in the case of a non-JDBC 4.0 compliant driver + connection.isValid(1); + } + catch (Throwable e) { + isValidSupported = false; + LOGGER.debug("{} - Connection.isValid() is not supported ({})", poolName, e.getMessage()); + } + + isValidChecked = true; + } + + return isValidSupported; + } + + /** + * 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.debug("{} - Statement.setQueryTimeout() is not supported ({})", poolName, e.getMessage()); + } + } + } + } + + /** + * Set the network timeout, if isUseNetworkTimeout is true 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.debug("{} - Connection.setNetworkTimeout() is not supported ({})", poolName, e.getMessage()); + } + } + } + + return 0; + } + + /** + * Set the network timeout, if isUseNetworkTimeout is true 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 isAutoCommit 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 isAutoCommit) throws SQLException + { + if (sql != null) { + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + + if (!isAutoCommit) { + connection.commit(); + } + } + } + } + + 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() != null ? config.getThreadFactory() : new DefaultThreadFactory("Hikari JDBC-timeout executor", true); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(threadFactory); + executor.allowCoreThreadTimeOut(true); + executor.setKeepAliveTime(15, TimeUnit.SECONDS); + netTimeoutExecutor = executor; + } + } + + private static class SynchronousExecutor implements Executor + { + /** {@inheritDoc} */ + @Override + public void execute(Runnable command) + { + try { + command.run(); + } + catch (Throwable t) { + LOGGER.debug("Exception executing {}", command, t); + } + } + } + + /** + * 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) TimeUnit.MILLISECONDS.toSeconds(Math.max(1000L, connectionTimeout))); + } + catch (SQLException e) { + LOGGER.warn("{} - Unable to set DataSource login timeout", poolName, e); + } + } + } + + /** + * 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(); + } +}