com.zaxxer.hikari.pool.HikariPool Maven / Gradle / Ivy
Show all versions of HikariCP Show documentation
/*
* 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 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.MetricsTrackerFactory;
import com.zaxxer.hikari.metrics.PoolStats;
import com.zaxxer.hikari.metrics.dropwizard.CodahaleHealthChecker;
import com.zaxxer.hikari.metrics.dropwizard.CodahaleMetricsTrackerFactory;
import com.zaxxer.hikari.metrics.dropwizard.Dropwizard5MetricsTrackerFactory;
import com.zaxxer.hikari.metrics.micrometer.MicrometerMetricsTrackerFactory;
import com.zaxxer.hikari.util.ConcurrentBag;
import com.zaxxer.hikari.util.ConcurrentBag.IBagStateListener;
import com.zaxxer.hikari.util.SuspendResumeLock;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLTransientConnectionException;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import static com.zaxxer.hikari.util.ClockSource.*;
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.UtilityElf.*;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* This is the primary connection pool class that provides the basic
* pooling behavior for HikariCP.
*
* @author Brett Wooldridge
*/
public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener
{
private final Logger logger = LoggerFactory.getLogger(HikariPool.class);
public static final int POOL_NORMAL = 0;
public static final int POOL_SUSPENDED = 1;
public static final int POOL_SHUTDOWN = 2;
public volatile int poolState;
private final long aliveBypassWindowMs = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500));
private final long housekeepingPeriodMs = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", SECONDS.toMillis(30));
private final long lifeTimeVarianceFactor = Math.min(40, Math.max(2, Long.getLong("com.zaxxer.hikari.lifeTimeVarianceFactor", 4))); // variance% = 100 / factor
private final boolean isRequestBoundariesEnabled = Boolean.getBoolean("com.zaxxer.hikari.enableRequestBoundaries");
private static final String EVICTED_CONNECTION_MESSAGE = "(connection was evicted)";
private static final String DEAD_CONNECTION_MESSAGE = "(connection is dead)";
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator();
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding ");
private final ThreadPoolExecutor addConnectionExecutor;
private final ThreadPoolExecutor closeConnectionExecutor;
private final ConcurrentBag connectionBag;
private final ProxyLeakTaskFactory leakTaskFactory;
private final SuspendResumeLock suspendResumeLock;
private final ScheduledExecutorService houseKeepingExecutorService;
private ScheduledFuture> houseKeeperTask;
/**
* Construct a HikariPool with the specified configuration.
*
* @param config a HikariConfig instance
*/
public HikariPool(final HikariConfig config)
{
super(config);
this.connectionBag = new ConcurrentBag<>(this);
this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
checkFailFast();
if (config.getMetricsTrackerFactory() != null) {
setMetricsTrackerFactory(config.getMetricsTrackerFactory());
}
else {
setMetricRegistry(config.getMetricRegistry());
}
setHealthCheckRegistry(config.getHealthCheckRegistry());
handleMBeans(this, true);
ThreadFactory threadFactory = config.getThreadFactory();
final int maxPoolSize = config.getMaximumPoolSize();
LinkedBlockingQueue addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize);
this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + ":connection-adder", threadFactory, new CustomDiscardPolicy());
this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + ":connection-closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) {
addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
final long startTime = currentTime();
while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) {
quietlySleep(MILLISECONDS.toMillis(100));
}
addConnectionExecutor.setCorePoolSize(1);
addConnectionExecutor.setMaximumPoolSize(1);
}
}
/**
* 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 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 Connection getConnection(final long hardTimeout) throws SQLException
{
suspendResumeLock.acquire();
final var startTime = currentTime();
try {
var timeout = hardTimeout;
do {
var poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception
}
final var now = currentTime();
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && isConnectionDead(poolEntry.connection))) {
closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
timeout = hardTimeout - elapsedMillis(startTime);
}
else {
metricsTracker.recordBorrowStats(poolEntry, startTime);
if (isRequestBoundariesEnabled) {
try {
poolEntry.connection.beginRequest();
} catch (SQLException e) {
logger.warn("beginRequest Failed for: {}, ({})", poolEntry.connection, e.getMessage());
}
}
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry));
}
} while (timeout > 0L);
metricsTracker.recordBorrowTimeoutStats(startTime);
throw createTimeoutException(startTime);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
finally {
suspendResumeLock.release();
}
}
/**
* Shutdown the pool, closing all idle connections and aborting or closing
* active connections.
*
* @throws InterruptedException thrown if the thread is interrupted during shutdown
*/
public synchronized void shutdown() throws InterruptedException
{
try {
poolState = POOL_SHUTDOWN;
if (addConnectionExecutor == null) { // pool never started
return;
}
logPoolState("Before shutdown ");
if (houseKeeperTask != null) {
houseKeeperTask.cancel(false);
houseKeeperTask = null;
}
softEvictConnections();
addConnectionExecutor.shutdown();
if (!addConnectionExecutor.awaitTermination(getLoginTimeout(), SECONDS)) {
logger.warn("Timed-out waiting for add connection executor to shutdown");
}
destroyHouseKeepingExecutorService();
connectionBag.close();
final var assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + ":connection-assassinator",
config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
try {
final var start = currentTime();
do {
abortActiveConnections(assassinExecutor);
softEvictConnections();
} while (getTotalConnections() > 0 && elapsedMillis(start) < SECONDS.toMillis(10));
}
finally {
assassinExecutor.shutdown();
if (!assassinExecutor.awaitTermination(10L, SECONDS)) {
logger.warn("Timed-out waiting for connection assassin to shutdown");
}
}
shutdownNetworkTimeoutExecutor();
closeConnectionExecutor.shutdown();
if (!closeConnectionExecutor.awaitTermination(10L, SECONDS)) {
logger.warn("Timed-out waiting for close connection executor to shutdown");
}
}
finally {
logPoolState("After shutdown ");
handleMBeans(this, false);
metricsTracker.close();
}
}
/**
* Evict a Connection from the pool.
*
* @param connection the Connection to evict (actually a {@link ProxyConnection})
*/
public void evictConnection(Connection connection)
{
var proxyConnection = (ProxyConnection) connection;
proxyConnection.cancelLeakTask();
try {
softEvictConnection(proxyConnection.getPoolEntry(), "(connection evicted by user)", !connection.isClosed() /* owner */);
}
catch (SQLException e) {
// unreachable in HikariCP, but we're still forced to catch it
}
}
/**
* Set a metrics registry to be used when registering metrics collectors. The HikariDataSource prevents this
* method from being called more than once.
*
* @param metricRegistry the metrics registry instance to use
*/
@SuppressWarnings("PackageAccessibility")
public void setMetricRegistry(Object metricRegistry)
{
if (metricRegistry != null && safeIsAssignableFrom(metricRegistry, "com.codahale.metrics.MetricRegistry")) {
setMetricsTrackerFactory(new CodahaleMetricsTrackerFactory((MetricRegistry) metricRegistry));
}
else if (metricRegistry != null && safeIsAssignableFrom(metricRegistry, "io.dropwizard.metrics5.MetricRegistry")) {
setMetricsTrackerFactory(new Dropwizard5MetricsTrackerFactory((io.dropwizard.metrics5.MetricRegistry) metricRegistry));
}
else if (metricRegistry != null && safeIsAssignableFrom(metricRegistry, "io.micrometer.core.instrument.MeterRegistry")) {
setMetricsTrackerFactory(new MicrometerMetricsTrackerFactory((MeterRegistry) metricRegistry));
}
else {
setMetricsTrackerFactory(null);
}
}
/**
* Set the MetricsTrackerFactory to be used to create the IMetricsTracker instance used by the pool.
*
* @param metricsTrackerFactory an instance of a class that subclasses MetricsTrackerFactory
*/
public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory)
{
if (metricsTrackerFactory != null) {
this.metricsTracker = new MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats()));
}
else {
this.metricsTracker = new NopMetricsTrackerDelegate();
}
}
/**
* Set the health check registry to be used when registering health checks. Currently only Codahale health
* checks are supported.
*
* @param healthCheckRegistry the health check registry instance to use
*/
public void setHealthCheckRegistry(Object healthCheckRegistry)
{
if (healthCheckRegistry != null) {
CodahaleHealthChecker.registerHealthChecks(this, config, (HealthCheckRegistry) healthCheckRegistry);
}
}
// ***********************************************************************
// IBagStateListener callback
// ***********************************************************************
/** {@inheritDoc} */
@Override
public void addBagItem(final int waiting)
{
if (waiting > addConnectionExecutor.getQueue().size())
addConnectionExecutor.submit(poolEntryCreator);
}
// ***********************************************************************
// HikariPoolMBean methods
// ***********************************************************************
/** {@inheritDoc} */
@Override
public int getActiveConnections()
{
return connectionBag.getCount(STATE_IN_USE);
}
/** {@inheritDoc} */
@Override
public int getIdleConnections()
{
return connectionBag.getCount(STATE_NOT_IN_USE);
}
/** {@inheritDoc} */
@Override
public int getTotalConnections()
{
return connectionBag.size();
}
/** {@inheritDoc} */
@Override
public int getThreadsAwaitingConnection()
{
return connectionBag.getWaitingThreadCount();
}
/** {@inheritDoc} */
@Override
public void softEvictConnections()
{
connectionBag.values().forEach(poolEntry -> softEvictConnection(poolEntry, "(connection evicted)", false /* not owner */));
}
/** {@inheritDoc} */
@Override
public 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 synchronized void resumePool()
{
if (poolState == POOL_SUSPENDED) {
poolState = POOL_NORMAL;
fillPool(false);
suspendResumeLock.resume();
}
}
// ***********************************************************************
// Package methods
// ***********************************************************************
/**
* Log the current pool state at debug level.
*
* @param prefix an optional prefix to prepend the log message
*/
void logPoolState(String... prefix)
{
if (logger.isDebugEnabled()) {
logger.debug("{} - {}stats (total={}/{}, idle={}/{}, active={}, waiting={})",
poolName, (prefix.length > 0 ? prefix[0] : ""),
getTotalConnections(), config.getMaximumPoolSize(), getIdleConnections(), config.getMinimumIdle(), getActiveConnections(), getThreadsAwaitingConnection());
}
}
/**
* Recycle PoolEntry (add back to the pool)
*
* @param poolEntry the PoolEntry to recycle
*/
@Override
void recycle(final PoolEntry poolEntry)
{
metricsTracker.recordConnectionUsage(poolEntry);
if (poolEntry.isMarkedEvicted()) {
closeConnection(poolEntry, EVICTED_CONNECTION_MESSAGE);
} else {
if (isRequestBoundariesEnabled) {
try {
poolEntry.connection.endRequest();
} catch (SQLException e) {
logger.warn("endRequest Failed for: {},({})", poolEntry.connection, e.getMessage());
}
}
connectionBag.requite(poolEntry);
}
}
/**
* Permanently close the real (underlying) connection (eat any exception).
*
* @param poolEntry poolEntry having the connection to close
* @param closureReason reason to close
*/
void closeConnection(final PoolEntry poolEntry, final String closureReason)
{
if (connectionBag.remove(poolEntry)) {
final var connection = poolEntry.close();
closeConnectionExecutor.execute(() -> {
quietlyCloseConnection(connection, closureReason);
if (poolState == POOL_NORMAL) {
fillPool(false);
}
});
}
}
@SuppressWarnings("unused")
int[] getPoolStateCounts()
{
return connectionBag.getStateCounts();
}
// ***********************************************************************
// Private methods
// ***********************************************************************
/**
* Creating new poolEntry. If maxLifetime is configured, create a future End-of-life task with variance from
* the maxLifetime time to ensure there is no massive die-off of Connections in the pool.
*/
private PoolEntry createPoolEntry()
{
try {
final var poolEntry = newPoolEntry();
final var maxLifetime = config.getMaxLifetime();
if (maxLifetime > 0) {
// default variance upto 25% of the maxLifetime (random)
final var variance = maxLifetime > 10_000L ? ThreadLocalRandom.current().nextLong( maxLifetime / lifeTimeVarianceFactor ) : 0L;
final var lifetime = maxLifetime - variance;
poolEntry.setFutureEol(houseKeepingExecutorService.schedule(new MaxLifetimeTask(poolEntry), lifetime, MILLISECONDS));
}
final long keepaliveTime = config.getKeepaliveTime();
if (keepaliveTime > 0) {
// variance up to 10% of the heartbeat time
final var variance = ThreadLocalRandom.current().nextLong(keepaliveTime / 10);
final var heartbeatTime = keepaliveTime - variance;
poolEntry.setKeepalive(houseKeepingExecutorService.scheduleWithFixedDelay(new KeepaliveTask(poolEntry), heartbeatTime, heartbeatTime, MILLISECONDS));
}
return poolEntry;
}
catch (ConnectionSetupException e) {
if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
logger.error("{} - Error thrown while acquiring connection from data source", poolName, e.getCause());
lastConnectionFailure.set(e);
}
}
catch (Exception e) {
if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
logger.debug("{} - Cannot acquire connection from data source", poolName, e);
}
}
return null;
}
/**
* Fill pool up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections.
*/
private synchronized void fillPool(final boolean isAfterAdd)
{
final var idle = getIdleConnections();
final var shouldAdd = getTotalConnections() < config.getMaximumPoolSize() && idle < config.getMinimumIdle();
if (shouldAdd) {
final var countToAdd = config.getMinimumIdle() - idle;
for (int i = 0; i < countToAdd; i++)
addConnectionExecutor.submit(isAfterAdd ? postFillPoolEntryCreator : poolEntryCreator);
}
else if (isAfterAdd) {
logger.debug("{} - Fill pool skipped, pool has sufficient level or currently being filled.", poolName);
}
}
/**
* Attempt to abort or close active connections.
*
* @param assassinExecutor the ExecutorService to pass to Connection.abort()
*/
private void abortActiveConnections(final ExecutorService assassinExecutor)
{
for (var poolEntry : connectionBag.values(STATE_IN_USE)) {
Connection connection = poolEntry.close();
try {
connection.abort(assassinExecutor);
}
catch (Throwable e) {
quietlyCloseConnection(connection, "(connection aborted during shutdown)");
}
finally {
connectionBag.remove(poolEntry);
}
}
}
/**
* If {@code initializationFailTimeout} is configured, check that we have DB connectivity.
*
* @throws PoolInitializationException if fails to create or validate connection
* @see HikariConfig#setInitializationFailTimeout(long)
*/
private void checkFailFast()
{
final var initializationFailTimeout = config.getInitializationFailTimeout();
if (initializationFailTimeout < 0) {
return;
}
final var startTime = currentTime();
do {
final var poolEntry = createPoolEntry();
if (poolEntry != null) {
if (config.getMinimumIdle() > 0) {
connectionBag.add(poolEntry);
logger.info("{} - Added connection {}", poolName, poolEntry.connection);
}
else {
quietlyCloseConnection(poolEntry.close(), "(initialization check complete and minimumIdle is zero)");
}
return;
}
if (getLastConnectionFailure() instanceof ConnectionSetupException) {
throwPoolInitializationException(getLastConnectionFailure().getCause());
}
quietlySleep(SECONDS.toMillis(1));
} while (elapsedMillis(startTime) < initializationFailTimeout);
if (initializationFailTimeout > 0) {
throwPoolInitializationException(getLastConnectionFailure());
}
}
/**
* Log the Throwable that caused pool initialization to fail, and then throw a PoolInitializationException with
* that cause attached.
*
* @param t the Throwable that caused the pool to fail to initialize (possibly null)
*/
private void throwPoolInitializationException(Throwable t)
{
destroyHouseKeepingExecutorService();
throw new PoolInitializationException(t);
}
/**
* "Soft" evict a Connection (/PoolEntry) from the pool. If this method is being called by the user directly
* through {@link com.zaxxer.hikari.HikariDataSource#evictConnection(Connection)} then {@code owner} is {@code true}.
*
* If the caller is the owner, or if the Connection is idle (i.e. can be "reserved" in the {@link ConcurrentBag}),
* then we can close the connection immediately. Otherwise, we leave it "marked" for eviction so that it is evicted
* the next time someone tries to acquire it from the pool.
*
* @param poolEntry the PoolEntry (/Connection) to "soft" evict from the pool
* @param reason the reason that the connection is being evicted
* @param owner true if the caller is the owner of the connection, false otherwise
* @return true if the connection was evicted (closed), false if it was merely marked for eviction
*/
private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner)
{
poolEntry.markEvicted();
if (owner || connectionBag.reserve(poolEntry)) {
closeConnection(poolEntry, reason);
return true;
}
return false;
}
/**
* Create/initialize the Housekeeping service {@link ScheduledExecutorService}. If the user specified an Executor
* to be used in the {@link HikariConfig}, then we use that. If no Executor was specified (typical), then create
* an Executor and configure it.
*
* @return either the user specified {@link ScheduledExecutorService}, or the one we created
*/
private ScheduledExecutorService initializeHouseKeepingExecutorService()
{
if (config.getScheduledExecutor() == null) {
final var threadFactory = Optional.ofNullable(config.getThreadFactory()).orElseGet(() -> new DefaultThreadFactory(poolName + ":housekeeper"));
final var executor = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy());
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
executor.setRemoveOnCancelPolicy(true);
return executor;
}
else {
return config.getScheduledExecutor();
}
}
/**
* Destroy (/shutdown) the Housekeeping service Executor, if it was the one that we created.
*/
private void destroyHouseKeepingExecutorService()
{
if (config.getScheduledExecutor() == null) {
houseKeepingExecutorService.shutdownNow();
}
}
/**
* Create a PoolStats instance that will be used by metrics tracking, with a pollable resolution of 1 second.
*
* @return a PoolStats instance
*/
private PoolStats getPoolStats()
{
return new PoolStats(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();
this.maxConnections = config.getMaximumPoolSize();
this.minConnections = config.getMinimumIdle();
}
};
}
/**
* Create a timeout exception (specifically, {@link SQLTransientConnectionException}) to be thrown, because a
* timeout occurred when trying to acquire a Connection from the pool. If there was an underlying cause for the
* timeout, e.g. a SQLException thrown by the driver while trying to create a new Connection, then use the
* SQL State from that exception as our own and additionally set that exception as the "next" SQLException inside
* our exception.
*
* As a side effect, log the timeout failure at DEBUG, and record the timeout failure in the metrics tracker.
*
* @param startTime the start time (timestamp) of the acquisition attempt
* @return a SQLException to be thrown from {@link #getConnection()}
*/
private SQLException createTimeoutException(long startTime)
{
logPoolState("Timeout failure ");
metricsTracker.recordConnectionTimeout();
String sqlState = null;
int errorCode = 0;
final var originalException = getLastConnectionFailure();
if (originalException instanceof SQLException) {
sqlState = ((SQLException) originalException).getSQLState();
errorCode = ((SQLException) originalException).getErrorCode();
}
final var connectionException = new SQLTransientConnectionException(
poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms " +
"(total=" + getTotalConnections() + ", active=" + getActiveConnections() + ", idle=" + getIdleConnections() + ", waiting=" + getThreadsAwaitingConnection() + ")",
sqlState, errorCode, originalException);
if (originalException instanceof SQLException) {
connectionException.setNextException((SQLException) originalException);
}
return connectionException;
}
// ***********************************************************************
// Non-anonymous Inner-classes
// ***********************************************************************
/**
* Creating and adding poolEntries (connections) to the pool.
*/
private final class PoolEntryCreator implements Callable
{
private final String loggingPrefix;
PoolEntryCreator()
{
this(null);
}
PoolEntryCreator(String loggingPrefix)
{
this.loggingPrefix = loggingPrefix;
}
@Override
public Boolean call()
{
var backoffMs = 10L;
var added = false;
try {
while (shouldContinueCreating()) {
final var poolEntry = createPoolEntry();
if (poolEntry != null) {
added = true;
connectionBag.add(poolEntry);
logger.debug("{} - Added connection {}", poolName, poolEntry.connection);
quietlySleep(30L);
break;
} else { // failed to get connection from db, sleep and retry
if (loggingPrefix != null && backoffMs % 50 == 0)
logger.debug("{} - Connection add failed, sleeping with backoff: {}ms", poolName, backoffMs);
quietlySleep(backoffMs);
backoffMs = Math.min(SECONDS.toMillis(5), backoffMs * 2);
}
}
}
finally {
if (added && loggingPrefix != null)
logPoolState(loggingPrefix);
else if (!added)
logPoolState("Connection not added, ");
}
// Pool is suspended, shutdown, or at max size
return Boolean.FALSE;
}
/**
* We only create connections if we need another idle connection or have threads still waiting
* for a new connection. Otherwise we bail out of the request to create.
*
* @return true if we should create a connection, false if the need has disappeared
*/
private synchronized boolean shouldContinueCreating() {
return poolState == POOL_NORMAL && getTotalConnections() < config.getMaximumPoolSize() &&
(getIdleConnections() < config.getMinimumIdle() || connectionBag.getWaitingThreadCount() > getIdleConnections());
}
}
/**
* The housekeeping task to retire and maintain minimum idle connections.
*/
private final class HouseKeeper implements Runnable
{
private volatile long previous = plusMillis(currentTime(), -housekeepingPeriodMs);
@SuppressWarnings("AtomicFieldUpdaterNotStaticFinal")
private final AtomicReferenceFieldUpdater catalogUpdater = AtomicReferenceFieldUpdater.newUpdater(PoolBase.class, String.class, "catalog");
@Override
public void run()
{
try {
// refresh values in case they changed via MBean
connectionTimeout = config.getConnectionTimeout();
validationTimeout = config.getValidationTimeout();
leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
if (config.getCatalog() != null && !config.getCatalog().equals(catalog)) {
catalogUpdater.set(HikariPool.this, config.getCatalog());
}
final var idleTimeout = config.getIdleTimeout();
final var now = currentTime();
// Detect retrograde time, allowing +128ms as per NTP spec.
if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs)) {
logger.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
poolName, elapsedDisplayString(previous, now));
previous = now;
softEvictConnections();
return;
}
else if (now > plusMillis(previous, (3 * housekeepingPeriodMs) / 2)) {
// No point evicting for forward clock motion, this merely accelerates connection retirement anyway
logger.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
}
previous = now;
if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
logPoolState("Before cleanup ");
final var notInUse = connectionBag.values(STATE_NOT_IN_USE);
var maxToRemove = notInUse.size() - config.getMinimumIdle();
for (PoolEntry entry : notInUse) {
if (maxToRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
closeConnection(entry, "(connection has passed idleTimeout)");
maxToRemove--;
}
}
logPoolState("After cleanup ");
}
else
logPoolState("Pool ");
fillPool(true); // Try to maintain minimum connections
}
catch (Exception e) {
logger.error("Unexpected exception in housekeeping task", e);
}
}
}
private final class MaxLifetimeTask implements Runnable
{
private final PoolEntry poolEntry;
MaxLifetimeTask(final PoolEntry poolEntry)
{
this.poolEntry = poolEntry;
}
public void run()
{
if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
addBagItem(connectionBag.getWaitingThreadCount());
}
}
}
private final class KeepaliveTask implements Runnable
{
private final PoolEntry poolEntry;
KeepaliveTask(final PoolEntry poolEntry)
{
this.poolEntry = poolEntry;
}
public void run()
{
if (connectionBag.reserve(poolEntry)) {
if (isConnectionDead(poolEntry.connection)) {
softEvictConnection(poolEntry, DEAD_CONNECTION_MESSAGE, true);
addBagItem(connectionBag.getWaitingThreadCount());
}
else {
connectionBag.unreserve(poolEntry);
logger.debug("{} - keepalive: connection {} is alive", poolName, poolEntry.connection);
}
}
}
}
public static class PoolInitializationException extends RuntimeException
{
private static final long serialVersionUID = 929872118275916520L;
/**
* Construct an exception, possibly wrapping the provided Throwable as the cause.
* @param t the Throwable to wrap
*/
public PoolInitializationException(Throwable t)
{
super("Failed to initialize pool: " + t.getMessage(), t);
}
}
}